From 28d4e1e17198747d2241457909b72e66152acb61 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 16 Dec 2025 20:05:44 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + submodules/AttachmentUI/BUILD | 2 + .../Sources/AttachmentPanel.swift | 189 +- .../MultilineTextWithEntitiesComponent.swift | 7 + .../Sources/PagerComponent.swift | 3 +- .../Sources/SheetComponent.swift | 1 - .../ItemListPermanentInviteLinkItem.swift | 4 +- .../Sources/CreateGiveawayController.swift | 14 +- .../Sources/PremiumIntroScreen.swift | 36 +- .../DeleteAccountOptionsController.swift | 20 +- .../State/ManagedEmojiGameUpdates.swift | 10 +- .../SyncCore/SyncCore_TelegramMediaDice.swift | 2 +- .../TelegramNotices/Sources/Notices.swift | 104 - .../Sources/PermissionContentNode.swift | 2 +- .../Sources/PermissionController.swift | 28 +- .../Sources/ServiceMessageStrings.swift | 11 + submodules/TelegramUI/BUILD | 1 + ...onent.swift => LegacyAlertComponent.swift} | 0 .../TelegramUI/Components/CameraScreen/BUILD | 2 + .../CameraScreen/Sources/CameraScreen.swift | 3 +- .../CameraScreen/Sources/ModeComponent.swift | 153 +- .../ChatMessageActionButtonsNode.swift | 1 + .../ChatMessageAnimatedStickerItemNode/BUILD | 1 + .../ChatMessageAnimatedStickerItemNode.swift | 111 + .../ChatMessageGiftBubbleContentNode.swift | 7 +- .../Sources/ManagedDiceAnimationNode.swift | 11 +- .../Sources/ChatEntityKeyboardInputNode.swift | 14 +- .../Components/EmojiGameStakeScreen/BUILD | 55 + .../Sources/EmojiGameStakeScreen.swift | 2187 +++++++++++++++++ .../Components/EntityKeyboard/BUILD | 3 + .../Sources/EmojiKeyboardItemLayer.swift | 10 +- .../Sources/EmojiPagerContentComponent.swift | 2 +- .../Sources/EntityKeyboard.swift | 17 +- .../EntityKeyboardBottomPanelButton.swift | 159 +- .../EntityKeyboardBottomPanelComponent.swift | 235 +- ...tyKeyboardTopContainerPanelComponent.swift | 15 +- .../EntityKeyboardTopPanelComponent.swift | 2 +- .../Sources/GiftAuctionActiveBidsScreen.swift | 13 +- .../Gifts/PeerTableCellComponent/BUILD | 26 + .../Sources/PeerTableCellComponent.swift | 123 + .../Components/Gifts/TableComponent/BUILD | 23 + .../Sources/TableComponent.swift | 375 +++ .../LiquidLens/Sources/LiquidLensView.swift | 24 +- .../PostSuggestionsSettingsScreen/BUILD | 3 + .../PostSuggestionsSettingsScreen.swift | 454 ++++ .../Stars/BalanceNeededScreen/BUILD | 2 + .../Sources/BalanceNeededScreen.swift | 50 +- .../Components/TabBarComponent/BUILD | 1 + .../Sources/TabBarComponent.swift | 59 +- .../Components/TabSelectionRecognizer/BUILD | 17 + .../Sources/TabSelectionRecognizer.swift | 58 + .../Sources/TextNodeWithEntities.swift | 26 +- .../Contents.json | 2 +- .../backspace_24.svg | 5 - .../backspace_30.pdf | Bin 0 -> 4847 bytes .../Contents.json | 2 +- .../keyboard_24.svg | 8 - .../keyboard_30.pdf | Bin 0 -> 6044 bytes .../Contents.json | 2 +- .../keyboard_2444.svg | 3 - .../settings_30 (4).pdf | Bin 0 -> 7917 bytes .../PanelFeaturedIcon.imageset/Contents.json | 2 +- .../PanelFeaturedIcon.imageset/Group 1.svg | 3 - .../trending_44.pdf | Bin 0 -> 8700 bytes .../Wearing.imageset/Contents.json | 12 + .../Wearing.imageset/wear_30 (2).pdf | Bin 0 -> 8186 bytes .../Premium/Dice.imageset/Contents.json | 12 + .../Premium/Dice.imageset/dice.pdf | Bin 0 -> 4749 bytes .../TelegramUI/Sources/ChatController.swift | 57 - .../ChatControllerDisplayDiceTooltip.swift | 125 + .../Sources/CreateChannelController.swift | 8 +- .../Sources/CreateGroupController.swift | 18 +- submodules/TelegramUI/Sources/OpenUrl.swift | 10 + .../Sources/UndoOverlayController.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 43 +- .../UrlHandling/Sources/UrlHandling.swift | 23 + 76 files changed, 4396 insertions(+), 620 deletions(-) rename submodules/TelegramUI/Components/AlertComponent/Sources/{AlertComponent.swift => LegacyAlertComponent.swift} (100%) create mode 100644 submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD create mode 100644 submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift create mode 100644 submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift create mode 100644 submodules/TelegramUI/Components/Gifts/TableComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift create mode 100644 submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD create mode 100644 submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf create mode 100644 submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 597ef1ad02..d0f6bdf30b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15623,3 +15623,6 @@ Error: %8$@"; "Chat.GiftPurchaseOffer.AcceptConfirmation.BadValue" = "The value of this gift is **%@** higher than the offer."; "Chat.GiftPurchaseOffer.AcceptConfirmation.Confirm" = "Confirm Sale"; + +"Notification.PremiumGift.DaysTitle_1" = "%@ Day Premium"; +"Notification.PremiumGift.DaysTitle_any" = "%@ Days Premium"; diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index d29a2ee478..21040b9100 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -44,6 +44,8 @@ swift_library( "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 1be4bd9cc0..4860336814 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -24,6 +24,8 @@ import LegacyMessageInputPanelInputView import ReactionSelectionNode import TopMessageReactions import GlassBackgroundComponent +import LiquidLens +import TabSelectionRecognizer private let legacyButtonSize = CGSize(width: 88.0, height: 49.0) private let glassButtonSize = CGSize(width: 72.0, height: 62.0) @@ -911,13 +913,21 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? private let containerNode: ASDisplayNode - private var backgroundView: GlassBackgroundView? + private var liquidLensView: LiquidLensView? private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode private let separatorNode: ASDisplayNode private let selectionNode: ASImageNode - private var buttonViews: [AnyHashable: ComponentHostView] = [:] + + private var itemsContainer = UIView() + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var selectedItemsContainer = UIView() + private var selectedItemViews: [AnyHashable: ComponentHostView] = [:] + private var itemSizes: [AnyHashable: CGSize] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? private var textInputPanelNode: AttachmentTextInputPanelNode? private var progressNode: LoadingProgressNode? @@ -987,7 +997,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.mainButtonNode = MainButtonNode() self.secondaryButtonNode = MainButtonNode() - + super.init() self.addSubnode(self.containerNode) @@ -995,7 +1005,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { switch style { case .glass: self.scrollNode.cornerRadius = glassButtonSize.height * 0.5 - self.scrollNode.addSubnode(self.selectionNode) + //self.scrollNode.addSubnode(self.selectionNode) case .legacy: self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.separatorNode) @@ -1419,6 +1429,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.view.accessibilityTraits = .tabBar } + func requestLayout(transition: ContainedViewLayoutTransition) { + if let layout = self.validLayout { + let _ = self.update(layout: layout, buttons: self.buttons, isSelecting: self.isSelecting, selectionCount: self.selectionCount, elevateProgress: self.elevateProgress, transition: transition) + } + } + @objc private func mainButtonPressed() { self.onMainButtonPressed() } @@ -1473,6 +1489,66 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } } + private func item(at point: CGPoint) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return id + } else { + let distance = abs(point.x - itemView.center.x) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let liquidLensView = self.liquidLensView else { + return + } + let location = recognizer.location(in: liquidLensView.contentView) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + + + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self.view).x + if let itemId = self.item(at: location) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.requestLayout(transition: .immediate) + } + case .ended, .cancelled: + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let index = self.buttons.firstIndex(where: { AnyHashable($0.key) == selectionGestureState.itemId }) else { + return + } + if self.selectionChanged(self.buttons[index]) { + self.selectedIndex = index + } + } + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + default: + break + } + } + func updateViews(transition: ComponentTransition) { guard let layout = self.validLayout else { return @@ -1533,13 +1609,19 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { var buttonTransition = transition let buttonView: ComponentHostView - if let current = self.buttonViews[type.key] { + let selectedButtonView: ComponentHostView + if let current = self.itemViews[type.key], let currentSelected = self.selectedItemViews[type.key] { buttonView = current + selectedButtonView = currentSelected } else { buttonTransition = .immediate buttonView = ComponentHostView() - self.buttonViews[type.key] = buttonView - self.scrollNode.view.addSubview(buttonView) + self.itemViews[type.key] = buttonView + self.itemsContainer.addSubview(buttonView) + + selectedButtonView = ComponentHostView() + self.selectedItemViews[type.key] = selectedButtonView + self.selectedItemsContainer.addSubview(selectedButtonView) } if case let .app(bot) = type { @@ -1578,7 +1660,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { style: self.panelStyle == .glass ? .glass : .legacy, type: type, isFirstOrLast: i == 0 || i == self.buttons.count - 1, - isSelected: i == self.selectedIndex, + isSelected: false, strings: self.presentationData.strings, theme: self.presentationData.theme, action: { [weak self] in @@ -1587,7 +1669,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { strongSelf.selectedIndex = i strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring))) - if strongSelf.buttons.count > 5, let button = strongSelf.buttonViews[type.key] { + if strongSelf.buttons.count > 5, let button = strongSelf.itemViews[type.key] { strongSelf.scrollNode.view.scrollRectToVisible(button.frame.insetBy(dx: -35.0, dy: 0.0), animated: true) } } @@ -1601,11 +1683,34 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { environment: {}, containerSize: CGSize(width: buttonWidth, height: self.buttonSize.height) ) + self.itemSizes[type.key] = actualButtonSize + + let _ = selectedButtonView.update( + transition: buttonTransition, + component: AnyComponent(AttachButtonComponent( + context: self.context, + style: self.panelStyle == .glass ? .glass : .legacy, + type: type, + isFirstOrLast: i == 0 || i == self.buttons.count - 1, + isSelected: true, + strings: self.presentationData.strings, + theme: self.presentationData.theme, + action: { + }, longPressAction: { [weak self] in + if let strongSelf = self, i == strongSelf.selectedIndex { + strongSelf.longPressed(type) + } + }) + ), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: self.buttonSize.height) + ) if i == self.selectedIndex { selectionFrame = CGRect(origin: CGPoint(x: buttonFrame.midX - actualButtonSize.width * 0.5, y: buttonFrame.minY), size: actualButtonSize) } buttonTransition.setFrame(view: buttonView, frame: buttonFrame) + buttonTransition.setFrame(view: selectedButtonView, frame: buttonFrame) var accessibilityTitle = "" switch type { case .gallery: @@ -1634,14 +1739,14 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { buttonView.accessibilityTraits = [.button] } var removeIds: [AnyHashable] = [] - for (id, itemView) in self.buttonViews { + for (id, itemView) in self.itemViews { if !validIds.contains(id) { removeIds.append(id) itemView.removeFromSuperview() } } for id in removeIds { - self.buttonViews.removeValue(forKey: id) + self.itemViews.removeValue(forKey: id) } selectionFrame = selectionFrame.insetBy(dx: 1.0, dy: 4.0) @@ -1916,29 +2021,51 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { textPanelWidth = layout.size.width - panelSideInset * 2.0 } + var selectionFrame = CGRect() + if self.selectedIndex >= 0 && self.selectedIndex < self.buttons.count, let itemView = self.itemViews[self.buttons[self.selectedIndex].key], let itemSize = self.itemSizes[self.buttons[self.selectedIndex].key] { + selectionFrame = CGRect(origin: CGPoint(x: itemView.center.x - itemSize.width / 2.0, y: itemView.frame.minY), size: itemSize).insetBy(dx: -3.0, dy: 0.0) + } + + let lensSelection: (x: CGFloat, width: CGFloat) + if let selectionGestureState = self.selectionGestureState { + lensSelection = (selectionGestureState.currentX, selectionFrame.width) + } else { + lensSelection = (selectionFrame.minX, selectionFrame.width) + } + let glassPanelHeight: CGFloat = 62.0 let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: self.buttonSize.height + insets.bottom)) if case .glass = self.panelStyle { - let backgroundView: GlassBackgroundView - if let current = self.backgroundView { + let backgroundView: LiquidLensView + if let current = self.liquidLensView { backgroundView = current } else { - backgroundView = GlassBackgroundView() + backgroundView = LiquidLensView(useBackgroundContainer: false) self.containerNode.view.addSubview(backgroundView) - self.containerNode.view.addSubview(self.scrollNode.view) - self.backgroundView = backgroundView + //self.containerNode.view.addSubview(self.scrollNode.view) + self.liquidLensView = backgroundView + + backgroundView.contentView.addSubview(self.itemsContainer) + backgroundView.selectedContentView.addSubview(self.selectedItemsContainer) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + self.tabSelectionRecognizer = tabSelectionRecognizer + backgroundView.addGestureRecognizer(tabSelectionRecognizer) } let panelSize = CGSize(width: isSelecting ? textPanelWidth : layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight) - let backgroundViewColor: UIColor - if self.presentationData.theme.overallDarkAppearance { - backgroundViewColor = self.presentationData.theme.list.modalBlocksBackgroundColor.withAlphaComponent(0.55) - } else { - backgroundViewColor = self.presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.75) - } - +// let backgroundViewColor: UIColor +// if self.presentationData.theme.overallDarkAppearance { +// backgroundViewColor = self.presentationData.theme.list.modalBlocksBackgroundColor.withAlphaComponent(0.55) +// } else { +// backgroundViewColor = self.presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.75) +// } +// let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) - backgroundView.update(size: panelSize, cornerRadius: isSelecting ? 20.0 : glassPanelHeight * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .custom, color: backgroundViewColor), transition: ComponentTransition(transition)) + //backgroundView.update(size: panelSize, cornerRadius: isSelecting ? 20.0 : glassPanelHeight * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .custom, color: backgroundViewColor), transition: ComponentTransition(transition)) + + backgroundView.update(size: panelSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) + transition.updatePosition(layer: backgroundView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) transition.updateBounds(layer: backgroundView.layer, bounds: CGRect(origin: .zero, size: panelSize)) } @@ -2002,7 +2129,14 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { let alphaTransition = ContainedViewLayoutTransition.animated(duration: isSelecting ? 0.1 : 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) containerTransition.updateTransformScale(node: self.scrollNode, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) - if let backgroundView = self.backgroundView { + + alphaTransition.updateAlpha(layer: self.itemsContainer.layer, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) + containerTransition.updateTransformScale(layer: self.itemsContainer.layer, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) + + alphaTransition.updateAlpha(layer: self.selectedItemsContainer.layer, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) + containerTransition.updateTransformScale(layer: self.selectedItemsContainer.layer, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) + + if let backgroundView = self.liquidLensView { containerTransition.updateTransformScale(layer: backgroundView.layer, scale: isAnyButtonVisible ? 0.85 : 1.0) } @@ -2022,7 +2156,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { alphaTransition.updateAlpha(node: textInputPanelNode, alpha: 1.0) let componentTransition = ComponentTransition.easeInOut(duration: 0.25) - componentTransition.animateBlur(layer: self.scrollNode.layer, fromRadius: 0.0, toRadius: 10.0) + componentTransition.animateBlur(layer: self.itemsContainer.layer, fromRadius: 0.0, toRadius: 10.0) let blurTransition = ComponentTransition.easeInOut(duration: 0.18) transition.animateTransformScale(node: textInputPanelNode.opaqueActionButtons, from: 0.01) @@ -2054,7 +2188,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { alphaTransition.updateAlpha(node: textInputPanelNode, alpha: 0.0) let componentTransition = ComponentTransition.easeInOut(duration: 0.25) - componentTransition.animateBlur(layer: self.scrollNode.layer, fromRadius: 10.0, toRadius: 0.0) + componentTransition.animateBlur(layer: self.itemsContainer.layer, fromRadius: 10.0, toRadius: 0.0) let blurTransition = ComponentTransition.easeInOut(duration: 0.18) transition.animateTransformScale(layer: textInputPanelNode.opaqueActionButtons.layer, from: CGPoint(x: 1.0, y: 1.0), to: CGPoint(x: 0.01, y: 0.01)) @@ -2187,4 +2321,3 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { self.updateViews(transition: .immediate) } } - diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index 7766272b59..2519f2f6eb 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -43,6 +43,7 @@ public final class MultilineTextWithEntitiesComponent: Component { public let handleSpoilers: Bool public let manualVisibilityControl: Bool public let resetAnimationsOnVisibilityChange: Bool + public let enableLooping: Bool public let displaysAsynchronously: Bool public let maxWidth: CGFloat? public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? @@ -71,6 +72,7 @@ public final class MultilineTextWithEntitiesComponent: Component { handleSpoilers: Bool = false, manualVisibilityControl: Bool = false, resetAnimationsOnVisibilityChange: Bool = false, + enableLooping: Bool = true, displaysAsynchronously: Bool = true, maxWidth: CGFloat? = nil, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, @@ -99,6 +101,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.handleSpoilers = handleSpoilers self.manualVisibilityControl = manualVisibilityControl self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange + self.enableLooping = enableLooping self.displaysAsynchronously = displaysAsynchronously self.maxWidth = maxWidth self.tapAction = tapAction @@ -142,6 +145,9 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange { return false } + if lhs.enableLooping != rhs.enableLooping { + return false + } if lhs.displaysAsynchronously != rhs.displaysAsynchronously { return false } @@ -250,6 +256,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textNode.tapAttributeAction = component.tapAction self.textNode.longTapAttributeAction = component.longTapAction self.textNode.spoilerColor = component.spoilerColor + self.textNode.enableLooping = component.enableLooping self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 42f1c2b07d..4b11ff0e5d 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -974,8 +974,9 @@ public final class PagerComponent: Component { public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 7ff09939d8..3d65cc98b5 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -403,8 +403,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } let fieldHeight: CGFloat = 52.0 - let fieldSpacing: CGFloat = 16.0 - let buttonHeight: CGFloat = 50.0 + let fieldSpacing: CGFloat = 10.0 + let buttonHeight: CGFloat = 52.0 let justCreatedCallSeparatorSpacing: CGFloat = 16.0 let justCreatedCallTextSpacing: CGFloat = 45.0 diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 523309152b..810c269d13 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -493,7 +493,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } }) case let .starsMore(theme, title): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { arguments.expandStars() }) case let .starsInfo(_, text): @@ -521,14 +521,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { if case let .channel(channel) = peer, case .group = channel.info { isGroup = true } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(isGroup ? presentationData.strings.BoostGift_GroupBoosts($0) : presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(isGroup ? presentationData.strings.BoostGift_GroupBoosts($0) : presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { }, setPeerIdWithRevealedOptions: { lhs, rhs in arguments.setItemIdWithRevealedOptions(lhs, rhs) }, removePeer: { id in arguments.removeChannel(id) }) case let .channelAdd(theme, text): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: { arguments.openChannelsSelection() }) case let .channelsInfo(_, text): @@ -582,7 +582,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { arguments.openPremiumIntro() }) case let .prizeDescription(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.showPrizeDescription = value @@ -590,7 +590,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } }) case let .prizeDescriptionText(_, placeholder, value, count): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in arguments.updateState { state in var updatedState = state updatedState.prizeDescription = value @@ -616,7 +616,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } else { text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever } - return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.dismissInput() var focus = false arguments.updateState { state in @@ -677,7 +677,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { case let .timeInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .winners(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.showWinners = value diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 3d9a74e164..2a3cf028fb 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1679,34 +1679,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } }) - - self.newPerksDisposable = combineLatest( - queue: Queue.mainQueue(), - ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager) - ).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in - guard let self else { - return - } - var newPerks: [String] = [] - if !dismissedBusinessBadge { - newPerks.append(PremiumPerk.business.identifier) - } - if !dismissedBusinessLinksBadge { - newPerks.append(PremiumPerk.businessLinks.identifier) - } - if !dismissedBusinessIntroBadge { - newPerks.append(PremiumPerk.businessIntro.identifier) - } - if !dismissedBusinessChatbotsBadge { - newPerks.append(PremiumPerk.businessChatBots.identifier) - } - self.newPerks = newPerks - self.updated() - }) - + self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] adsEnabled in guard let self else { @@ -2219,7 +2192,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .todo case .business: demoSubject = .business - let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: demoSubject = .doubleLimits } @@ -2418,7 +2390,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) }) - let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessIntro: let _ = (accountContext.sharedContext.makeBusinessIntroSetupScreenInitialData(context: accountContext) |> take(1) @@ -2428,7 +2399,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext, initialData: initialData)) }) - let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessLinks: let _ = (accountContext.sharedContext.makeBusinessLinksSetupScreenInitialData(context: accountContext) |> take(1) @@ -2438,7 +2408,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessLinksSetupScreen(context: accountContext, initialData: initialData)) }) - let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: fatalError() } @@ -2457,13 +2426,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .businessAwayMessage case .businessChatBots: demoSubject = .businessChatBots - let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessIntro: demoSubject = .businessIntro - let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .businessLinks: demoSubject = .businessLinks - let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: fatalError() } diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index df1a0039c9..66a7f5025b 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -102,43 +102,43 @@ private enum DeleteAccountOptionsEntry: ItemListNodeEntry, Equatable { let arguments = arguments as! DeleteAccountOptionsArguments switch self { case let .changePhoneNumber(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.changePhoneNumber, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.changePhoneNumber() }) case let .addAccount(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteAddAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.addAccount() }) case let .changePrivacy(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.security, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setupPrivacy() }) case let .setTwoStepAuth(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteSetTwoStepAuth, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setupTwoStepAuth() }) case let .setPasscode(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteSetPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.setPasscode() }) case let .clearCache(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.dataAndStorage, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearCache() }) case let .clearSyncedContacts(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.clearSynced, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearSyncedContacts() }) case let .deleteChats(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.deleteChats, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.deleteChats() }) case let .contactSupport(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.support, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.contactSupport() }) case let .deleteAccount(_, title): - return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteAccount() }) } diff --git a/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift index 293ea88955..710f78904b 100644 --- a/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedEmojiGameUpdates.swift @@ -11,11 +11,11 @@ public enum EmojiGameInfo: Codable, Equatable { } public struct Info: Codable, Equatable { - let gameHash: String - let previousStake: Int64 - let currentStreak: Int32 - let parameters: [Int32] - let playsLeft: Int32? + public let gameHash: String + public let previousStake: Int64 + public let currentStreak: Int32 + public let parameters: [Int32] + public let playsLeft: Int32? } case available(Info) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift index 0040b4457f..c6bdf9d464 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaDice.swift @@ -4,7 +4,7 @@ import Postbox public final class TelegramMediaDice: Media, Equatable { public struct GameOutcome: Equatable { let seed: Data - let tonAmount: Int64 + public let tonAmount: Int64 } public let emoji: String diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index b1e4e9ace3..0734033617 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -193,12 +193,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case incomingVideoMessagePlayOnceTip = 62 case outgoingVideoMessagePlayOnceTip = 63 case savedMessageTagLabelSuggestion = 65 - case dismissedBusinessBadge = 68 case monetizationIntroDismissed = 70 case businessBotMessageTooltip = 71 - case dismissedBusinessIntroBadge = 72 - case dismissedBusinessLinksBadge = 73 - case dismissedBusinessChatbotsBadge = 74 case captionAboveMediaTooltip = 75 case channelSendGiftTooltip = 76 case starGiftWearTips = 77 @@ -519,10 +515,6 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.savedMessageTagLabelSuggestion.key) } - static func dismissedBusinessBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessBadge.key) - } - static func dismissedBirthdayPremiumGiftTip(peerId: PeerId) -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: dismissedBirthdayPremiumGiftTipNamespace), key: noticeKey(peerId: peerId, key: 0)) } @@ -543,18 +535,6 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.businessBotMessageTooltip.key) } - static func dismissedBusinessIntroBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessIntroBadge.key) - } - - static func dismissedBusinessLinksBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessLinksBadge.key) - } - - static func dismissedBusinessChatbotsBadge() -> NoticeEntryKey { - return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessChatbotsBadge.key) - } - static func captionAboveMediaTooltip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.captionAboveMediaTooltip.key) } @@ -2154,27 +2134,6 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } - - public static func setDismissedBusinessBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } public static func dismissedBirthdayPremiumGiftTip(accountManager: AccountManager, peerId: PeerId) -> Signal { return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBirthdayPremiumGiftTip(peerId: peerId)) @@ -2286,69 +2245,6 @@ public struct ApplicationSpecificNotice { } } - public static func setDismissedBusinessLinksBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessLinksBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessLinksBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessLinksBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } - - public static func setDismissedBusinessIntroBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessIntroBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessIntroBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessIntroBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } - - public static func setDismissedBusinessChatbotsBadge(accountManager: AccountManager) -> Signal { - return accountManager.transaction { transaction -> Void in - if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { - transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessChatbotsBadge(), entry) - } - } - |> ignoreValues - } - - public static func dismissedBusinessChatbotsBadge(accountManager: AccountManager) -> Signal { - return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessChatbotsBadge()) - |> map { view -> Bool in - if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { - return true - } else { - return false - } - } - |> take(1) - } - public static func getCaptionAboveMediaTooltip(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Int32 in if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.captionAboveMediaTooltip())?.get(ApplicationSpecificCounterNotice.self) { diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift index 04a7a16029..9843334806 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift @@ -107,7 +107,7 @@ public final class PermissionContentNode: ASDisplayNode { self.textNode.displaysAsynchronously = false self.textNode.isAccessibilityElement = true - self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 9.0, isShimmering: true) + self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), glass: true, height: 52.0, cornerRadius: 26.0, isShimmering: true) self.footerNode = ImmediateTextNode() self.footerNode.textAlignment = .center diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index 5d9e099c44..d0c2f05323 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -36,15 +36,17 @@ public final class PermissionController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.splashScreen = splashScreen - let navigationBarPresentationData: NavigationBarPresentationData - if splashScreen { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) - } else { - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) - } +// let navigationBarPresentationData: NavigationBarPresentationData +// if splashScreen { +// navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, style: .glass), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) +// } else { + let navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, style: .glass) +// } super.init(navigationBarPresentationData: navigationBarPresentationData) + self._hasGlassStyle = true + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.updateThemeAndStrings() @@ -86,15 +88,15 @@ public final class PermissionController: ViewController { private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - let navigationBarPresentationData: NavigationBarPresentationData - if self.splashScreen { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) - } else { - navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) - } +// let navigationBarPresentationData: NavigationBarPresentationData +// if self.splashScreen { +// navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) +// } else { + let navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, style: .glass) +// } self.navigationBar?.updatePresentationData(navigationBarPresentationData, transition: .immediate) - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) + //self.navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) if self.navigationItem.rightBarButtonItem != nil { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed)) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index c139bcc6ad..5a0c2ea7fb 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1764,6 +1764,17 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, resultTitleString = strings.Conversation_StoryExpiredMentionTextOutgoing(compactPeerName) } attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else if let dice = media as? TelegramMediaDice, let gameOutcome = dice.gameOutcome { + if let value = dice.value, value > 1 { + //TODO:localize + let value = formatTonAmountText(gameOutcome.tonAmount, dateTimeFormat: dateTimeFormat) + let attributedText = NSMutableAttributedString(string: "You won $\(value)", font: titleFont, textColor: primaryTextColor) + if let range = attributedText.string.range(of: "$") { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedText.string)) + } + attributedString = attributedText + } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 0b4b6a185b..db1bf254d7 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -508,6 +508,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode", "//submodules/TelegramUI/Components/Settings/PasskeysScreen", "//submodules/TelegramUI/Components/Gifts/GiftDemoScreen", + "//submodules/TelegramUI/Components/EmojiGameStakeScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift similarity index 100% rename from submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift rename to submodules/TelegramUI/Components/AlertComponent/Sources/LegacyAlertComponent.swift diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index b169af473a..5d92b5d4c8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -95,6 +95,8 @@ swift_library( "//submodules/TelegramCallsUI", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index b29f30a514..8f05aa0097 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2097,7 +2097,7 @@ private final class CameraScreenComponent: CombinedComponent { if !isSticker, case .none = component.cameraState.recording, component.cameraState.isStreaming == .none && !state.isTransitioning && hasAllRequiredAccess && component.cameraState.collageProgress < 1.0 - .ulpOfOne { let availableModeControlSize: CGSize if isTablet { - availableModeControlSize = CGSize(width: panelWidth, height: 120.0) + availableModeControlSize = CGSize(width: floor(panelWidth), height: 120.0) } else { availableModeControlSize = availableSize } @@ -2131,7 +2131,6 @@ private final class CameraScreenComponent: CombinedComponent { modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0 + controlsBottomInset + 16.0) } context.add(modeControl - .clipsToBounds(true) .position(modeControlPosition) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index cfb581ad45..1448df3c0d 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -5,6 +5,8 @@ import ComponentFlow import MultilineTextComponent import TelegramPresentationData import GlassBackgroundComponent +import LiquidLens +import TabSelectionRecognizer extension CameraMode { func title(strings: PresentationStrings) -> String { @@ -70,28 +72,17 @@ final class ModeComponent: Component { final class View: UIView, ComponentTaggedView { private var component: ModeComponent? + private var state: EmptyComponentState? final class ItemView: HighlightTrackingButton { - var pressed: () -> Void = { - - } - init() { super.init(frame: .zero) - - self.isExclusiveTouch = true - - self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) } required init(coder: NSCoder) { preconditionFailure() } - @objc func buttonPressed() { - self.pressed() - } - func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize { let accentColor: UIColor let normalColor: UIColor @@ -113,9 +104,15 @@ final class ModeComponent: Component { } private var backgroundView = UIView() - private var glassContainerView = GlassBackgroundContainerView() - private var selectionView = GlassBackgroundView() - private var itemViews: [Int32: ItemView] = [:] + private var backgroundContainer = GlassBackgroundContainerView() + + private let liquidLensView: LiquidLensView + + private var itemViews: [AnyHashable: ItemView] = [:] + private var selectedItemViews: [AnyHashable: ItemView] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { @@ -128,16 +125,23 @@ final class ModeComponent: Component { } init() { + self.liquidLensView = LiquidLensView(useBackgroundContainer: false) + super.init(frame: CGRect()) self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11) self.backgroundView.layer.cornerRadius = 24.0 - + self.layer.allowsGroupOpacity = true self.addSubview(self.backgroundView) - self.backgroundView.addSubview(self.glassContainerView) - self.glassContainerView.contentView.addSubview(self.selectionView) + self.backgroundView.addSubview(self.backgroundContainer) + + self.backgroundContainer.contentView.addSubview(self.liquidLensView) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + self.tabSelectionRecognizer = tabSelectionRecognizer + self.liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } required init?(coder aDecoder: NSCoder) { @@ -162,14 +166,69 @@ final class ModeComponent: Component { override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.backgroundView.frame.contains(point) } - - func update(component: ModeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - self.component = component - let isTablet = component.isTablet - let updatedMode = component.updatedMode + private func item(at point: CGPoint) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return id + } else { + let distance = abs(point.x - itemView.center.x) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } + let location = recognizer.location(in: self.liquidLensView.contentView) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: location) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.state?.updated(transition: .immediate, isLocal: true) + } + case .ended, .cancelled: + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else { + return + } + component.updatedMode(item) + } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + default: + break + } + } + + func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let isTablet = component.isTablet - self.glassContainerView.isHidden = component.isTablet self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) let inset: CGFloat = 23.0 @@ -180,25 +239,34 @@ final class ModeComponent: Component { var selectedCenter = itemFrame.minX var selectedFrame = itemFrame - var validKeys: Set = Set() + var validKeys: Set = Set() for mode in component.availableModes.reversed() { let id = mode.rawValue validKeys.insert(id) let itemView: ItemView - if let current = self.itemViews[id] { + let selectedItemView: ItemView + if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] { itemView = current + selectedItemView = currentSelected } else { itemView = ItemView() - self.backgroundView.addSubview(itemView) + itemView.isUserInteractionEnabled = false self.itemViews[id] = itemView - } - itemView.pressed = { - updatedMode(mode) + self.liquidLensView.contentView.addSubview(itemView) + + selectedItemView = ItemView() + selectedItemView.isUserInteractionEnabled = false + self.selectedItemViews[id] = selectedItemView + self.liquidLensView.selectedContentView.addSubview(selectedItemView) } - let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: mode == component.currentMode, tintColor: component.tintColor) + let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) itemView.bounds = CGRect(origin: .zero, size: itemSize) + + let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor) + selectedItemView.bounds = CGRect(origin: .zero, size: itemSize) + itemFrame = CGRect(origin: itemFrame.origin, size: itemSize) if mode == component.currentMode { @@ -207,12 +275,14 @@ final class ModeComponent: Component { if isTablet { itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY) + selectedItemView.center = itemView.center if mode == component.currentMode { selectedCenter = itemFrame.midY } itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing) } else { itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + selectedItemView.center = itemView.center if mode == component.currentMode { selectedCenter = itemFrame.midX } @@ -221,7 +291,7 @@ final class ModeComponent: Component { i += 1 } - var removeKeys: [Int32] = [] + var removeKeys: [AnyHashable] = [] for (id, itemView) in self.itemViews { if !validKeys.contains(id) { removeKeys.append(id) @@ -248,12 +318,19 @@ final class ModeComponent: Component { } let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) - transition.setFrame(view: self.glassContainerView, frame: containerFrame) + transition.setFrame(view: self.backgroundContainer, frame: containerFrame) - let selectionFrame = selectedFrame.insetBy(dx: -20.0, dy: 3.0) - self.glassContainerView.update(size: containerFrame.size, isDark: true, transition: .immediate) - self.selectionView.update(size: selectionFrame.size, cornerRadius: selectionFrame.height * 0.5, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0xffffff, alpha: 0.16)), transition: transition) - transition.setFrame(view: self.selectionView, frame: selectionFrame) + let selectionFrame = selectedFrame.insetBy(dx: -23.0, dy: 3.0) + let lensSelection: (x: CGFloat, width: CGFloat) + if let selectionGestureState = self.selectionGestureState { + lensSelection = (selectionGestureState.currentX, selectionFrame.width) + } else { + lensSelection = (selectionFrame.minX, selectionFrame.width) + } + + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) + self.liquidLensView.update(size: containerFrame.size, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: selectionFrame.height), isDark: true, isLifted: self.selectionGestureState != nil, isCollapsed: false, transition: transition) + self.backgroundContainer.update(size: containerFrame.size, isDark: true, transition: .immediate) return size } @@ -264,7 +341,7 @@ final class ModeComponent: Component { } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 80adb8f267..cb3801d027 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -440,6 +440,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { if node.iconNode == nil { let iconNode = ASImageNode() iconNode.contentMode = .center + iconNode.isUserInteractionEnabled = false node.iconNode = iconNode node.addSubnode(iconNode) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD index b5507029f4..30fadb7618 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/WallpaperBackgroundNode", "//submodules/LocalMediaResources", "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/ChatControllerInteraction", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 001202a781..21815278ab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -47,6 +47,7 @@ import ManagedDiceAnimationNode import MessageHaptics import ChatMessageTransitionNode import ChatMessageSuggestedPostInfoNode +import TelegramStringFormatting private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -99,6 +100,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? + private let labelNode: TextNodeWithEntities + private var labelBackgroundNode: WallpaperBubbleBackgroundNode? + private let labelBackgroundMaskNode: ASImageNode + private var cachedMaskLabelBackgroundImage: (CGPoint, UIImage, [CGRect])? + private var selectionNode: ChatMessageSelectionNode? private var deliveryFailedNode: ChatMessageDeliveryFailedNode? private var shareButtonNode: ChatMessageShareButton? @@ -161,6 +167,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.isUserInteractionEnabled = false + self.labelNode = TextNodeWithEntities() + self.labelNode.textNode.isUserInteractionEnabled = false + self.labelNode.textNode.displaysAsynchronously = false + + self.labelBackgroundMaskNode = ASImageNode() + super.init(rotated: rotated) self.containerNode.shouldBegin = { [weak self] location in @@ -469,9 +481,22 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } else if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode { if let value = telegramDice.value { + let wasRolling = diceNode.isRolling diceNode.setState(value == 0 ? .rolling : .value(value, true)) + + if wasRolling && !diceNode.isRolling { + Queue.mainQueue().after(3.0, { + self.labelNode.textNode.alpha = 1.0 + self.labelBackgroundNode?.alpha = 1.0 + self.labelNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.labelBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + }) + } } else { diceNode.setState(.rolling) + + self.labelNode.textNode.alpha = 0.0 + self.labelBackgroundNode?.alpha = 0.0 } } else if self.telegramFile == nil && self.telegramDice == nil { let (emoji, fitz) = item.message.text.basicEmoji @@ -816,6 +841,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) + let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode) + let cachedMaskLabelBackgroundImage = self.cachedMaskLabelBackgroundImage + let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) @@ -850,6 +878,44 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let avatarInset: CGFloat var hasAvatar = false + let labelAttributedText = universalServiceMessageString(presentationData: (item.presentationData.theme.theme, item.presentationData.theme.wallpaper), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + var labelRects = labelLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + labelRects[index].size.width = labelRects[index + j].size.width + } + } + } + } + } + for i in 0 ..< labelRects.count { + labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0)) + labelRects[i].size.height = 22.0 + labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) + } + + let backgroundMaskImage: (CGPoint, UIImage)? + var backgroundMaskUpdated = false + if labelLayout.size.height > 0.0 { + if let (currentOffset, currentImage, currentRects) = cachedMaskLabelBackgroundImage, currentRects == labelRects { + backgroundMaskImage = (currentOffset, currentImage) + } else { + backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false) + backgroundMaskUpdated = true + } + } else { + backgroundMaskImage = nil + } + switch item.chatLocation { case let .peer(peerId): if peerId != item.context.account.peerId { @@ -1493,6 +1559,51 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + + let _ = labelApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) + strongSelf.labelNode.textNode.frame = labelFrame + if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 { + strongSelf.addSubnode(strongSelf.labelNode.textNode) + } + + let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + if let (offset, image) = backgroundMaskImage { + if strongSelf.labelBackgroundNode == nil { + if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundNode.alpha = strongSelf.labelNode.textNode.alpha + strongSelf.labelBackgroundNode = backgroundNode + strongSelf.insertSubnode(backgroundNode, at: 0) + } + } + + if backgroundMaskUpdated, let backgroundNode = strongSelf.labelBackgroundNode { + if labelRects.count == 1 { + backgroundNode.clipsToBounds = true + backgroundNode.cornerRadius = labelRects[0].height / 2.0 + backgroundNode.view.mask = nil + } else { + backgroundNode.clipsToBounds = false + backgroundNode.cornerRadius = 0.0 + backgroundNode.view.mask = strongSelf.labelBackgroundMaskNode.view + } + } + + if let backgroundNode = strongSelf.labelBackgroundNode { + backgroundNode.layer.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size) + } + strongSelf.labelBackgroundMaskNode.image = image + strongSelf.labelBackgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size) + + strongSelf.cachedMaskLabelBackgroundImage = (offset, image, labelRects) + } + var updatedImageFrame: CGRect var contextContentFrame: CGRect if let _ = emojiString { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 730e74f103..b19d721b66 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -455,7 +455,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { switch action.action { case let .giftPremium(_, _, daysValue, _, _, giftText, giftEntities): months = max(3, Int32(round(Float(daysValue) / 30.0))) - if months == 12 { + if daysValue < 30 { + title = item.presentationData.strings.Notification_PremiumGift_DaysTitle(daysValue) + } else if months == 12 { title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) } else { title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) @@ -513,7 +515,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { title = item.presentationData.strings.Notification_StarsGiveaway_Title let starsString = item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(clamping: count)).replacingOccurrences(of: " ", with: "\u{00A0}") text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, starsString).string - case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities): + case let .giftCode(_, fromGiveaway, unclaimed, channelId, daysValue, _, _, _, _, giftText, giftEntities): + let monthsValue = max(3, Int32(round(Float(daysValue) / 30.0))) if channelId == nil { months = monthsValue if months == 12 { diff --git a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift index 7f38fa1efb..d6e4efc807 100644 --- a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift @@ -100,7 +100,7 @@ public struct InteractiveEmojiConfiguration { } public static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration { - if let data = appConfiguration.data, let emojis = data["emojies_send_dice"] as? [String] { + if let data = appConfiguration.data, var emojis = data["emojies_send_dice"] as? [String] { var successParameters: [String: InteractiveEmojiSuccessParameters] = [:] if let success = data["emojies_send_dice_success"] as? [String: [String: Double]] { for (key, dict) in success { @@ -109,6 +109,11 @@ public struct InteractiveEmojiConfiguration { } } } + #if DEBUG + if !emojis.contains("🎲") { + emojis.append("🎲") + } + #endif return InteractiveEmojiConfiguration(emojis: emojis, successParameters: successParameters) } else { return .defaultValue @@ -126,6 +131,10 @@ public final class ManagedDiceAnimationNode: ManagedAnimationNode { private let configuration = Promise() private let emojis = Promise<[TelegramMediaFile]>() + public var isRolling: Bool { + return self.diceState == .rolling + } + public var success: (() -> Void)? public init(context: AccountContext, emoji: String) { diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 3daaeff1aa..efac328858 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -33,6 +33,8 @@ import LegacyMessageInputPanelInputView import AttachmentTextInputPanelNode import GlassBackgroundComponent +private let keyboardCornerRadius: CGFloat = 30.0 + public final class EmptyInputView: UIView, UIInputViewAudioFeedback { public var enableInputClicksWhenVisible: Bool { return true @@ -490,7 +492,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.clippingView = UIView() self.clippingView.clipsToBounds = true - self.clippingView.layer.cornerRadius = 20.0 + self.clippingView.layer.cornerRadius = keyboardCornerRadius self.entityKeyboardView = ComponentHostView() @@ -1904,8 +1906,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { theme: interfaceState.theme, strings: interfaceState.strings, isContentInFocus: isVisible, - containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -34.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset), - topPanelInsets: UIEdgeInsets(), + containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -42.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset), + topPanelInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0), emojiContent: emojiContent, stickerContent: stickerContent, maskContent: nil, @@ -2030,16 +2032,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundFrame.size.height += 32.0 if backgroundChromeView.image == nil { - backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 20.0 * 2.0, height: 20.0 * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear) + backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: keyboardCornerRadius * 2.0, height: keyboardCornerRadius * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear) } if backgroundTintView.image == nil { - backgroundTintView.image = generateStretchableFilledCircleImage(diameter: 20.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + backgroundTintView.image = generateStretchableFilledCircleImage(diameter: keyboardCornerRadius * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) } backgroundTintView.tintColor = interfaceState.theme.chat.inputMediaPanel.backgroundColor transition.updateFrame(view: backgroundView, frame: backgroundFrame) backgroundView.updateColor(color: .clear, forceKeepBlur: true, transition: .immediate) - backgroundView.update(size: backgroundFrame.size, cornerRadius: 20.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition) + backgroundView.update(size: backgroundFrame.size, cornerRadius: keyboardCornerRadius, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition) transition.updateFrame(view: backgroundChromeView, frame: backgroundFrame.insetBy(dx: -1.0, dy: 0.0)) diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD new file mode 100644 index 0000000000..0200885e73 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD @@ -0,0 +1,55 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmojiGameStakeScreen", + module_name = "EmojiGameStakeScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ItemListUI", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/UndoUI", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/ChatScheduleTimeController", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift new file mode 100644 index 0000000000..49e1b27f26 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift @@ -0,0 +1,2187 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import BundleIconComponent +import ButtonComponent +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import TelegramStringFormatting +import UndoUI +import ListActionItemComponent +import PresentationDataUtils +import BalanceNeededScreen +import GlassBarButtonComponent +import GlassBackgroundComponent +import StarsBalanceOverlayComponent +import LottieComponent +import LottieComponentResourceContent +import EdgeEffect +import PlainButtonComponent + +private let amountTag = GenericComponentViewTag() + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let gameInfo: EmojiGameInfo.Info + let controller: () -> ViewController? + let dismiss: () -> Void + + init( + context: AccountContext, + gameInfo: EmojiGameInfo.Info, + controller: @escaping () -> ViewController?, + dismiss: @escaping () -> Void + ) { + self.context = context + self.gameInfo = gameInfo + self.controller = controller + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + return true + } + + static var body: (CombinedComponentContext) -> CGSize { + let description = Child(BalancedTextComponent.self) + let resultsTitle = Child(MultilineTextComponent.self) + let results = Child(VStack.self) + let resultsFooter = Child(MultilineTextWithEntitiesComponent.self) + let amountSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + + let body: (CombinedComponentContext) -> CGSize = { (context: CombinedComponentContext) -> CGSize in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + state.component = component + + let controller = environment.controller + + let theme = environment.theme.withModalBlocksBackground() + //let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + var contentSize = CGSize(width: context.availableSize.width, height: 75.0) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + //TODO:localize + let description = description.update( + component: BalancedTextComponent( + text: .markdown(text: "An experimental feature for Telegram Premium users.", attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + description.size.height * 0.5)) + ) + contentSize.height += description.size.height + contentSize.height += 32.0 + + let resultsTitle = resultsTitle.update( + component: MultilineTextComponent(text: .plain(NSAttributedString( + string: "RESULTS AND RETURNS".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + ))), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(resultsTitle + .position(CGPoint(x: textSideInset + resultsTitle.size.width * 0.5, y: contentSize.height + resultsTitle.size.height * 0.5)) + ) + contentSize.height += resultsTitle.size.height + contentSize.height += 6.0 + + let resultSpacing: CGFloat = 8.0 + let resultSize = CGSize(width: (context.availableSize.width - sideInset * 2.0 - resultSpacing * 3.0) / 4.0, height: 64.0) + let doubleResultSize = CGSize(width: resultSize.width * 2.0 + resultSpacing, height: resultSize.height) + + var resultValue1: Int32 = 0 + var resultValue2: Int32 = 300 + var resultValue3: Int32 = 600 + var resultValue4: Int32 = 1300 + var resultValue5: Int32 = 1600 + var resultValue6: Int32 = 2000 + var resultValue7: Int32 = 20000 + if context.component.gameInfo.parameters.count == 7 { + resultValue1 = context.component.gameInfo.parameters[0] + resultValue2 = context.component.gameInfo.parameters[1] + resultValue3 = context.component.gameInfo.parameters[2] + resultValue4 = context.component.gameInfo.parameters[3] + resultValue5 = context.component.gameInfo.parameters[4] + resultValue6 = context.component.gameInfo.parameters[5] + resultValue7 = context.component.gameInfo.parameters[6] + } + + let results = results.update( + component: VStack([ + AnyComponentWithIdentity(id: "first", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[1]] }, value: resultValue1, size: resultSize) + )), + AnyComponentWithIdentity(id: 2, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[2]] }, value: resultValue2, size: resultSize) + )), + AnyComponentWithIdentity(id: 3, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[3]] }, value: resultValue3, size: resultSize) + )), + AnyComponentWithIdentity(id: 4, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[4]] }, value: resultValue4, size: resultSize) + )) + ], spacing: resultSpacing) + )), + AnyComponentWithIdentity(id: "second", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: 5, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[5]] }, value: resultValue5, size: resultSize) + )), + AnyComponentWithIdentity(id: 6, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[6]] }, value: resultValue6, size: resultSize) + )), + AnyComponentWithIdentity(id: 7, component: AnyComponent( + ResultCellComponent(context: component.context, theme: environment.theme, files: state.emojiFiles.flatMap { [$0[6], $0[6], $0[6]] }, value: resultValue7, size: doubleResultSize) + )), + ], spacing: resultSpacing) + )) + ], spacing: resultSpacing), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(results + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + results.size.height * 0.5)) + ) + contentSize.height += results.size.height + contentSize.height += 7.0 + + let resultsFooterAttributedText = NSMutableAttributedString( + string: "A streak resets after 3 # or a stake change.", + font: Font.regular(13.0), + textColor: theme.list.freeTextColor + ) + if let emojiFile = state.emojiFiles?[6] { + let range = (resultsFooterAttributedText.string as NSString).range(of: "#") + if range.location != NSNotFound { + resultsFooterAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range) + } + } + + let resultsFooter = resultsFooter.update( + component: MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .clear, + text: .plain(resultsFooterAttributedText), + maximumNumberOfLines: 0, + enableLooping: true + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(resultsFooter + .position(CGPoint(x: textSideInset + resultsFooter.size.width * 0.5, y: contentSize.height + resultsFooter.size.height * 0.5)) + ) + contentSize.height += resultsFooter.size.height + contentSize.height += 39.0 + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + + let configuration = EmojiGameStakeConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + var amountLabel = "" + if let tonUsdRate = configuration.tonUsdRate, let value = state.amount?.value, value > 0 { + amountLabel = "~\(formatTonUsdValue(value, divide: true, rate: tonUsdRate, dateTimeFormat: environment.dateTimeFormat))" + } + + let amountItems: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity( + id: "amount", + component: AnyComponent( + AmountFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + secondaryColor: theme.list.itemSecondaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + accentColor: theme.list.itemAccentColor, + value: state.amount?.value, + minValue: 0, + forceMinValue: false, + allowZero: true, + maxValue: nil, + placeholderText: "Amount", + labelText: amountLabel, + currency: .ton, + dateTimeFormat: presentationData.dateTimeFormat, + amountUpdated: { [weak state] amount in + state?.amount = amount.flatMap { StarsAmount(value: $0, nanos: 0) } + state?.updated() + }, + tag: amountTag + ) + ) + ), + AnyComponentWithIdentity(id: "presets", component: AnyComponent( + AmountPresetsListItemComponent( + context: component.context, + theme: theme, + values: [ + 100000000, + 1000000000, + 2000000000, + 5000000000, + 10000000000, + 20000000000 + ], + valueSelected: { [weak state] value in + guard let state else { + return + } + state.amount = StarsAmount(value: value, nanos: 0) + if let controller = controller() as? EmojiGameStakeScreen { + controller.dismissInput() + state.updated() + controller.resetValue() + } + } + ) + )) + ] + + //TODO:localize + let amountSection = amountSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "STAKE".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: amountItems + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += amountSection.size.height + contentSize.height += 24.0 + + + //TODO:localize + var buttonItems: [AnyComponentWithIdentity] = [] + buttonItems.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Premium/Dice", tintColor: theme.list.itemCheckColors.foregroundColor)))) + buttonItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Save and Roll", font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)))) + + 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.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonItems, spacing: 7.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + if let state, let amount = state.amount, let controller = controller() as? EmojiGameStakeScreen { + controller.complete(amount: amount) + } + } + ), + 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: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + + if environment.inputHeight > 0.0 { + contentSize.height += 15.0 + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + } else { + contentSize.height += buttonInsets.bottom + } + + return contentSize + } + + return body + } + + final class State: ComponentState { + fileprivate let context: AccountContext + + fileprivate var component: SheetContent + + fileprivate var forceUpdateAmount = false + fileprivate var amount: StarsAmount? + fileprivate var currency: CurrencyAmount.Currency = .ton + + var cachedChevronImage: (UIImage, PresentationTheme)? + + var emojiFiles: [TelegramMediaFile]? + var emojiFilesDisposable: Disposable? + + init(component: SheetContent) { + self.context = component.context + self.component = component + + let amount: StarsAmount? = StarsAmount(value: component.gameInfo.previousStake, nanos: 0) + let currency: CurrencyAmount.Currency = .ton + + self.currency = currency + self.amount = amount + + super.init() + + self.emojiFilesDisposable = (self.context.engine.stickers.loadedStickerPack(reference: .dice("🎲"), forceActualized: false) + |> mapToSignal { stickerPack -> Signal<[TelegramMediaFile], NoError> in + switch stickerPack { + case let .result(_, items, _): + var emojiStickers: [TelegramMediaFile] = [] + for item in items { + emojiStickers.append(item.file._parse()) + } + return .single(emojiStickers) + default: + return .complete() + } + } + |> deliverOnMainQueue).start(next: { [weak self] files in + guard let self else { + return + } + self.emojiFiles = files + self.updated() + }) + } + + deinit { + self.emojiFilesDisposable?.dispose() + } + } + + func makeState() -> State { + return State(component: self) + } +} + +private final class EmojiGameStakeSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let gameInfo: EmojiGameInfo.Info + + init( + context: AccountContext, + gameInfo: EmojiGameInfo.Info + ) { + self.context = context + self.gameInfo = gameInfo + } + + static func ==(lhs: EmojiGameStakeSheetComponent, rhs: EmojiGameStakeSheetComponent) -> Bool { + return true + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let dismiss: (Bool) -> Void = { 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) + } + } + } + + let theme = environment.theme.withModalBlocksBackground() + + var buttonItems: [AnyComponentWithIdentity] = [] + buttonItems.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(Image(image: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), tintColor: theme.list.itemCheckColors.foregroundColor, size: CGSize(width: 16.0, height: 18.0))))) + buttonItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Save and Roll", font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)))) + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + gameInfo: context.component.gameInfo, + controller: { + return controller() + }, + dismiss: { + dismiss(true) + } + )), + titleItem: AnyComponent( + Text(text: "Emoji Stake", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor) + ), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + bottomItem: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonItems, spacing: 7.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { + dismiss(true) + } + ) + ), + backgroundColor: .color(theme.list.blocksBackgroundColor), + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + 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 final class EmojiGameStakeScreen: ViewControllerComponentContainer { + private let context: AccountContext + fileprivate let completion: (StarsAmount) -> Void + + fileprivate let balanceOverlay = ComponentView() + private var showBalance = true + + public init( + context: AccountContext, + gameInfo: EmojiGameInfo.Info, + completion: @escaping (StarsAmount) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: EmojiGameStakeSheetComponent( + context: context, + gameInfo: gameInfo + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + + self.context.tonContext?.load(force: true) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func dismissInput() { + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + view.deactivateInput() + } + } + + fileprivate func resetValue() { + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + view.resetValue() + } + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } + + func complete(amount: StarsAmount) { + if let tonState = self.context.tonContext?.currentState, tonState.balance < amount { + let needed = amount - tonState.balance + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } + self.push(BalanceNeededScreen( + context: self.context, + amount: needed, + buttonAction: { [weak self] in + self?.context.sharedContext.applicationBindings.openUrl(fragmentUrl) + } + )) + } else { + self.completion(amount) + self.dismissAnimated() + } + } + + func dismissBalanceOverlay() { + if let view = self.balanceOverlay.view, view.superview != nil { + view.alpha = 0.0 + view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in + view.removeFromSuperview() + view.alpha = 1.0 + }) + } + } + + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if self.showBalance { + let context = self.context + let insets = layout.insets(options: .statusBar) + let balanceSize = self.balanceOverlay.update( + transition: .immediate, + component: AnyComponent( + StarsBalanceOverlayComponent( + context: context, + peerId: context.account.peerId, + theme: context.sharedContext.currentPresentationData.with { $0 }.theme, + currency: .ton, + action: { [weak self] in + guard let self else { + return + } + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } + context.sharedContext.applicationBindings.openUrl(fragmentUrl) + + self.dismissAnimated() + } + ) + ), + environment: {}, + containerSize: layout.size + ) + if let view = self.balanceOverlay.view { + if view.superview == nil { + self.view.addSubview(view) + + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - balanceSize.width) / 2.0), y: insets.top + 5.0), size: balanceSize) + } + } else if let view = self.balanceOverlay.view, view.superview != nil { + view.alpha = 0.0 + view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { _ in + view.removeFromSuperview() + view.alpha = 1.0 + }) + } + } +} + +private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { + private let currency: CurrencyAmount.Currency + private let dateTimeFormat: PresentationDateTimeFormat + + private let textField: UITextField + private let minValue: Int64 + private let forceMinValue: Bool + private let allowZero: Bool + private let maxValue: Int64 + private let updated: (Int64) -> Void + private let isEmptyUpdated: (Bool) -> Void + private let animateError: () -> Void + private let focusUpdated: (Bool) -> Void + + init?(textField: UITextField, currency: CurrencyAmount.Currency, dateTimeFormat: PresentationDateTimeFormat, minValue: Int64, forceMinValue: Bool, allowZero: Bool, maxValue: Int64, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, animateError: @escaping () -> Void, focusUpdated: @escaping (Bool) -> Void) { + self.textField = textField + self.currency = currency + self.dateTimeFormat = dateTimeFormat + self.minValue = minValue + self.forceMinValue = forceMinValue + self.allowZero = allowZero + self.maxValue = maxValue + self.updated = updated + self.isEmptyUpdated = isEmptyUpdated + self.animateError = animateError + self.focusUpdated = focusUpdated + + super.init() + } + + func amountFrom(text: String) -> Int64 { + var amount: Int64? + if !text.isEmpty { + switch self.currency { + case .stars: + if let value = Int64(text) { + amount = value + } + case .ton: + let scale: Int64 = 1_000_000_000 // 10⁹ (one “nano”) + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let dot = text.firstIndex(of: decimalSeparator) { + // Slices for the parts on each side of the dot + var wholeSlice = String(text[.. 9 { + fractionStr = String(fractionStr.prefix(9)) // trim extra digits + } else { + fractionStr = fractionStr.padding( + toLength: 9, withPad: "0", startingAt: 0) // pad with zeros + } + + // Convert and combine + if let whole = Int64(wholeSlice), + let frac = Int64(fractionStr) { + + let whole = min(whole, Int64.max / scale) + + amount = whole * scale + frac + } + } else if let whole = Int64(text) { // string had no dot at all + let whole = min(whole, Int64.max / scale) + + amount = whole * scale + } + } + } + return amount ?? 0 + } + + func onTextChanged(text: String) { + self.updated(self.amountFrom(text: text)) + self.isEmptyUpdated(text.isEmpty) + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var acceptZero = false + if case .ton = self.currency, self.minValue < 1_000_000_000 { + acceptZero = true + } + + var newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if newText.contains(where: { c in + switch c { + case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": + return false + default: + if case .ton = self.currency { + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, c == decimalSeparator { + return false + } + } + return true + } + }) { + return false + } + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, newText.count(where: { $0 == decimalSeparator }) > 1 { + return false + } + + switch self.currency { + case .stars: + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0")) { + newText.removeFirst() + textField.text = newText + self.onTextChanged(text: newText) + return false + } + case .ton: + var fixedText = false + if let decimalSeparator = self.dateTimeFormat.decimalSeparator.first, let index = newText.firstIndex(of: decimalSeparator) { + let fractionalString = newText[newText.index(after: index)...] + if fractionalString.count > 2 { + newText = String(newText[newText.startIndex ..< newText.index(index, offsetBy: 3)]) + fixedText = true + } + } + + if newText == self.dateTimeFormat.decimalSeparator { + if !acceptZero { + newText.removeFirst() + } else { + newText = "0\(newText)" + } + fixedText = true + } + + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0\(self.dateTimeFormat.decimalSeparator)")) { + newText.removeFirst() + fixedText = true + } + + if fixedText { + textField.text = newText + self.onTextChanged(text: newText) + return false + } + } + + let amount: Int64 = self.amountFrom(text: newText) + if self.forceMinValue && amount < self.minValue { + switch self.currency { + case .stars: + textField.text = "\(self.minValue)" + case .ton: + textField.text = "\(formatTonAmountText(self.minValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.onTextChanged(text: self.textField.text ?? "") + self.animateError() + return false + } else if amount > self.maxValue { + switch self.currency { + case .stars: + textField.text = "\(self.maxValue)" + case .ton: + textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: self.dateTimeFormat.decimalSeparator, groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.onTextChanged(text: self.textField.text ?? "") + self.animateError() + return false + } + + self.onTextChanged(text: newText) + + return true + } +} + +public final class AmountFieldComponent: Component { + public typealias EnvironmentType = Empty + + let textColor: UIColor + let secondaryColor: UIColor + let placeholderColor: UIColor + let accentColor: UIColor + let value: Int64? + let minValue: Int64? + let forceMinValue: Bool + let allowZero: Bool + let maxValue: Int64? + let placeholderText: String + let textFieldOffset: CGPoint + let labelText: String? + let currency: CurrencyAmount.Currency + let dateTimeFormat: PresentationDateTimeFormat + let amountUpdated: (Int64?) -> Void + let tag: AnyObject? + + public init( + textColor: UIColor, + secondaryColor: UIColor, + placeholderColor: UIColor, + accentColor: UIColor, + value: Int64?, + minValue: Int64?, + forceMinValue: Bool, + allowZero: Bool, + maxValue: Int64?, + placeholderText: String, + textFieldOffset: CGPoint = .zero, + labelText: String?, + currency: CurrencyAmount.Currency, + dateTimeFormat: PresentationDateTimeFormat, + amountUpdated: @escaping (Int64?) -> Void, + tag: AnyObject? = nil + ) { + self.textColor = textColor + self.secondaryColor = secondaryColor + self.placeholderColor = placeholderColor + self.accentColor = accentColor + self.value = value + self.minValue = minValue + self.forceMinValue = forceMinValue + self.allowZero = allowZero + self.maxValue = maxValue + self.placeholderText = placeholderText + self.textFieldOffset = textFieldOffset + self.labelText = labelText + self.currency = currency + self.dateTimeFormat = dateTimeFormat + self.amountUpdated = amountUpdated + self.tag = tag + } + + public static func ==(lhs: AmountFieldComponent, rhs: AmountFieldComponent) -> Bool { + if lhs.textColor != rhs.textColor { + return false + } + if lhs.secondaryColor != rhs.secondaryColor { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.allowZero != rhs.allowZero { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.placeholderText != rhs.placeholderText { + return false + } + if lhs.labelText != rhs.labelText { + return false + } + if lhs.currency != rhs.currency { + return false + } + return true + } + + public final class View: UIView, ListSectionComponent.ChildView, UITextFieldDelegate, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private let placeholderView: ComponentView + private let icon = ComponentView() + private let textField: TextFieldNodeView + private var starsFormatter: AmountFieldStarsFormatter? + private var tonFormatter: AmountFieldStarsFormatter? + private let labelView: ComponentView + + private var component: AmountFieldComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var didSetValueOnce = false + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? + public let separatorInset: CGFloat = 16.0 + + public override init(frame: CGRect) { + self.placeholderView = ComponentView() + self.textField = TextFieldNodeView(frame: .zero) + self.labelView = ComponentView() + + super.init(frame: frame) + + self.addSubview(self.textField) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func activateInput() { + self.textField.becomeFirstResponder() + } + + public func deactivateInput() { + self.textField.resignFirstResponder() + } + + public func selectAll() { + self.textField.selectAll(nil) + } + + public func animateError() { + self.textField.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + } + + public func resetValue() { + guard let component = self.component, let value = component.value else { + return + } + var text = "" + switch component.currency { + case .stars: + text = "\(value)" + case .ton: + text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.textField.text = text + self.placeholderView.view?.isHidden = !(self.textField.text ?? "").isEmpty + } + + func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.textField.textColor = component.textColor + if self.component?.currency != component.currency || ((self.textField.text ?? "").isEmpty && !self.didSetValueOnce) { + if let value = component.value, value != .zero { + var text = "" + switch component.currency { + case .stars: + text = "\(value)" + case .ton: + text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))" + } + self.textField.text = text + self.placeholderView.view?.isHidden = !text.isEmpty + } else { + self.textField.text = "" + } + self.didSetValueOnce = true + } + self.textField.font = Font.regular(17.0) + + self.textField.returnKeyType = .done + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .none + + if self.component?.currency != component.currency { + switch component.currency { + case .stars: + self.textField.delegate = self + self.textField.keyboardType = .numberPad + if self.starsFormatter == nil { + self.starsFormatter = AmountFieldStarsFormatter( + textField: self.textField, + currency: component.currency, + dateTimeFormat: component.dateTimeFormat, + minValue: component.minValue ?? 0, + forceMinValue: component.forceMinValue, + allowZero: component.allowZero, + maxValue: component.maxValue ?? Int64.max, + updated: { [weak self] value in + guard let self, let component = self.component else { + return + } + if !self.isUpdating { + component.amountUpdated(value == 0 ? nil : value) + } + }, + isEmptyUpdated: { [weak self] isEmpty in + guard let self else { + return + } + self.placeholderView.view?.isHidden = !isEmpty + }, + animateError: { [weak self] in + guard let self else { + return + } + self.animateError() + }, + focusUpdated: { _ in + } + ) + } + self.tonFormatter = nil + self.textField.delegate = self.starsFormatter + case .ton: + self.textField.keyboardType = .decimalPad + if self.tonFormatter == nil { + self.tonFormatter = AmountFieldStarsFormatter( + textField: self.textField, + currency: component.currency, + dateTimeFormat: component.dateTimeFormat, + minValue: component.minValue ?? 0, + forceMinValue: component.forceMinValue, + allowZero: component.allowZero, + maxValue: component.maxValue ?? Int64.max, + updated: { [weak self] value in + guard let self, let component = self.component else { + return + } + if !self.isUpdating { + component.amountUpdated(value == 0 ? nil : value) + } + }, + isEmptyUpdated: { [weak self] isEmpty in + guard let self else { + return + } + self.placeholderView.view?.isHidden = !isEmpty + }, + animateError: { [weak self] in + guard let self else { + return + } + self.animateError() + }, + focusUpdated: { _ in + } + ) + } + self.starsFormatter = nil + self.textField.delegate = self.tonFormatter + } + self.textField.reloadInputViews() + } + + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width, height: 52.0) + + let sideInset: CGFloat = 16.0 + var leftInset: CGFloat = 16.0 + + let iconName: String + var iconTintColor: UIColor? + let iconMaxSize: CGSize? + var iconOffset = CGPoint() + switch component.currency { + case .stars: + iconName = "Premium/Stars/StarLarge" + iconMaxSize = CGSize(width: 22.0, height: 22.0) + case .ton: + iconName = "Ads/TonBig" + iconTintColor = component.accentColor + iconMaxSize = CGSize(width: 18.0, height: 18.0) + iconOffset = CGPoint(x: 3.0, y: 1.0) + } + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: iconName, + tintColor: iconTintColor, + maxSize: iconMaxSize + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(x: iconOffset.x + 15.0, y: iconOffset.y - 1.0 + floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + } + + leftInset += 24.0 + 6.0 + + let placeholderSize = self.placeholderView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent( + Text( + text: component.placeholderText, + font: Font.regular(17.0), + color: component.placeholderColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + if let placeholderComponentView = self.placeholderView.view { + if placeholderComponentView.superview == nil { + self.insertSubview(placeholderComponentView, at: 0) + } + + placeholderComponentView.frame = CGRect(origin: CGPoint(x: leftInset, y: -1.0 + floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) + placeholderComponentView.isHidden = !(self.textField.text ?? "").isEmpty + } + + if let labelText = component.labelText { + let labelSize = self.labelView.update( + transition: .immediate, + component: AnyComponent( + Text( + text: labelText, + font: Font.regular(17.0), + color: component.secondaryColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + if let labelView = self.labelView.view { + if labelView.superview == nil { + self.insertSubview(labelView, at: 0) + } + + labelView.frame = CGRect(origin: CGPoint(x: size.width - sideInset - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0) + 1.0 - UIScreenPixel), size: labelSize) + } + } else if let labelView = self.labelView.view, labelView.superview != nil { + labelView.removeFromSuperview() + } + + self.textField.frame = CGRect(x: leftInset + component.textFieldOffset.x, y: 4.0 + component.textFieldOffset.y, width: size.width - 30.0, height: 44.0) + + 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) + } +} + +private final class ResultCellComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let files: [TelegramMediaFile]? + let value: Int32 + let size: CGSize + + init( + context: AccountContext, + theme: PresentationTheme, + files: [TelegramMediaFile]?, + value: Int32, + size: CGSize + ) { + self.context = context + self.theme = theme + self.files = files + self.value = value + self.size = size + } + + static func ==(lhs: ResultCellComponent, rhs: ResultCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.files != rhs.files { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + final class View: UIView { + private var component: ResultCellComponent? + + private let background = ComponentView() + private let emoji = ComponentView() + private let title = ComponentView() + + override init(frame: CGRect) { + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ResultCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let size = component.size + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent(color: component.theme.list.itemBlocksBackgroundColor, cornerRadius: .value(22.0), smoothCorners: true)), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let value = Double(component.value) / 1000.0 + let titleString = String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + + var items: [AnyComponentWithIdentity] = [] + if let files = component.files { + for file in files { + items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent( + context: component.context, + file: file, + attemptSynchronously: true, + providesPlaceholder: true + ), + placeholderColor: component.theme.list.mediaPlaceholderColor, + startingPosition: .end, + size: CGSize(width: 50.0, height: 50.0), + loop: false + ) + ))) + } + } + let emojiSize = self.emoji.update( + transition: transition, + component: AnyComponent( + HStack(items, spacing: -18.0) + ), + environment: {}, + containerSize: availableSize + ) + let emojiFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - emojiSize.width) / 2.0), y: -9.0), size: emojiSize) + if let emojiView = self.emoji.view { + if emojiView.superview == nil { + self.addSubview(emojiView) + } + transition.setFrame(view: emojiView, frame: emojiFrame) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "×\(titleString)", font: Font.semibold(11.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 10.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + return size + } + } + + 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 final class AmountPresetsListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let values: [Int64] + let valueSelected: (Int64) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + values: [Int64], + valueSelected: @escaping (Int64) -> Void + ) { + self.context = context + self.theme = theme + self.values = values + self.valueSelected = valueSelected + } + + static func ==(lhs: AmountPresetsListItemComponent, rhs: AmountPresetsListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.values != rhs.values { + return false + } + return true + } + + final class View: UIView { + private var component: AmountPresetsListItemComponent? + + private var itemViews: [Int64: ComponentView] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AmountPresetsListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + + let sideInset: CGFloat = 16.0 + let spacing: CGFloat = 6.0 + let itemSize = CGSize(width: floorToScreenPixels((availableSize.width - sideInset * 2.0 - spacing * 2.0) / 3.0), height: 28.0) + + var itemOrigin = CGPoint(x: sideInset, y: sideInset) + for value in component.values { + let itemView: ComponentView + if let current = self.itemViews[value] { + itemView = current + } else { + itemView = ComponentView() + self.itemViews[value] = itemView + } + let _ = itemView.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(AmountPresetComponent( + context: component.context, + theme: component.theme, + value: value + )), + action: { + component.valueSelected(value) + }, + animateScale: false + )), + environment: {}, + containerSize: itemSize + ) + var itemFrame = CGRect(origin: itemOrigin, size: itemSize) + if itemFrame.maxX > availableSize.width { + itemOrigin = CGPoint(x: sideInset, y: itemOrigin.y + itemSize.height + spacing) + itemFrame.origin = itemOrigin + } + if let itemView = itemView.view { + if itemView.superview == nil { + self.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + itemOrigin.x += itemSize.width + spacing + } + + let size = CGSize(width: availableSize.width, height: itemOrigin.y + itemSize.height + sideInset) + return size + } + } + + 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 final class AmountPresetComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let value: Int64 + + init( + context: AccountContext, + theme: PresentationTheme, + value: Int64 + ) { + self.context = context + self.theme = theme + self.value = value + } + + static func ==(lhs: AmountPresetComponent, rhs: AmountPresetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView { + private var component: AmountPresetComponent? + + private let background = ComponentView() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AmountPresetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let size = availableSize + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent(color: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), cornerRadius: .minEdge, smoothCorners: false)), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let attributedText = NSMutableAttributedString(string: "$ \(formatTonAmountText(component.value, dateTimeFormat: presentationData.dateTimeFormat))", font: Font.semibold(14.0), textColor: component.theme.list.itemAccentColor) + if let range = attributedText.string.range(of: "$") { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .clear, + text: .plain(attributedText) + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + return size + } + } + + 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 struct EmojiGameStakeConfiguration { + static var defaultValue: EmojiGameStakeConfiguration { + return EmojiGameStakeConfiguration(tonUsdRate: nil) + } + + let tonUsdRate: Double? + + fileprivate init(tonUsdRate: Double?) { + self.tonUsdRate = tonUsdRate + } + + static func with(appConfiguration: AppConfiguration) -> EmojiGameStakeConfiguration { + if let data = appConfiguration.data { + var tonUsdRate: Double? + if let value = data["ton_usd_rate"] as? Double { + tonUsdRate = value + } + + return EmojiGameStakeConfiguration(tonUsdRate: tonUsdRate) + } else { + return .defaultValue + } + } +} + + + + + + + + + + +public final class ResizableSheetComponentEnvironment: Equatable { + public let theme: PresentationTheme + public let statusBarHeight: CGFloat + public let safeInsets: UIEdgeInsets + public let metrics: LayoutMetrics + public let deviceMetrics: DeviceMetrics + public let isDisplaying: Bool + public let isCentered: Bool + public let regularMetricsSize: CGSize? + public let dismiss: (Bool) -> Void + + public init( + theme: PresentationTheme, + statusBarHeight: CGFloat, + safeInsets: UIEdgeInsets, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + isDisplaying: Bool, + isCentered: Bool, + regularMetricsSize: CGSize?, + dismiss: @escaping (Bool) -> Void + ) { + self.theme = theme + self.statusBarHeight = statusBarHeight + self.safeInsets = safeInsets + self.metrics = metrics + self.deviceMetrics = deviceMetrics + self.isDisplaying = isDisplaying + self.isCentered = isCentered + self.regularMetricsSize = regularMetricsSize + self.dismiss = dismiss + } + + public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.statusBarHeight != rhs.statusBarHeight { + return false + } + if lhs.safeInsets != rhs.safeInsets { + return false + } + if lhs.metrics != rhs.metrics { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.isDisplaying != rhs.isDisplaying { + return false + } + if lhs.isCentered != rhs.isCentered { + return false + } + if lhs.regularMetricsSize != rhs.regularMetricsSize { + return false + } + return true + } +} + +public final class ResizableSheetComponent: Component { + public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment) + + public class ExternalState { + public fileprivate(set) var contentHeight: CGFloat + + public init() { + self.contentHeight = 0.0 + } + } + + public enum BackgroundColor: Equatable { + case color(UIColor) + } + + public let content: AnyComponent + public let titleItem: AnyComponent? + public let leftItem: AnyComponent? + public let rightItem: AnyComponent? + public let bottomItem: AnyComponent? + public let backgroundColor: BackgroundColor + public let externalState: ExternalState? + public let animateOut: ActionSlot> + + public init( + content: AnyComponent, + titleItem: AnyComponent? = nil, + leftItem: AnyComponent? = nil, + rightItem: AnyComponent? = nil, + bottomItem: AnyComponent? = nil, + backgroundColor: BackgroundColor, + externalState: ExternalState? = nil, + animateOut: ActionSlot>, + ) { + self.content = content + self.titleItem = titleItem + self.leftItem = leftItem + self.rightItem = rightItem + self.bottomItem = bottomItem + self.backgroundColor = backgroundColor + self.externalState = externalState + self.animateOut = animateOut + } + + public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.titleItem != rhs.titleItem { + return false + } + if lhs.leftItem != rhs.leftItem { + return false + } + if lhs.rightItem != rhs.rightItem { + return false + } + if lhs.bottomItem != rhs.bottomItem { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.animateOut != rhs.animateOut { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var containerCornerRadius: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.containerCornerRadius = containerCornerRadius + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { + public final class Tag { + public init() { + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private let dimView: UIView + private let containerView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let topEdgeEffectView: EdgeEffectView + private let contentView: ComponentView + + private var titleItemView: ComponentView? + private var leftItemView: ComponentView? + private var rightItemView: ComponentView? + private var bottomItemView: ComponentView? + + private let backgroundHandleView: UIImageView + + private var ignoreScrolling: Bool = false + + private var component: ResizableSheetComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ResizableSheetComponentEnvironment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 40.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 40.0 + + self.backgroundHandleView = UIImageView() + + self.navigationBarContainer = SparseContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.topEdgeEffectView = EdgeEffectView() + self.topEdgeEffectView.clipsToBounds = true + self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topEdgeEffectView.layer.cornerRadius = 40.0 + + self.contentView = ComponentView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.addSubview(self.containerView) + self.containerView.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.containerView.addSubview(self.navigationBarContainer) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismissAnimated() + } + } + + func dismissAnimated() { + guard let environment = self.environment else { + return + } + self.endEditing(true) + environment.dismiss(true) + } + + private func updateScrolling(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width + let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 + let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius + + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction + let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) + let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction + + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + transition.setTransform(view: self.containerView, transform: containerTransform) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) + } + + private var didPlayAppearanceAnimation = false + func animateIn() { + self.didPlayAppearanceAnimation = true + + 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) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + func update(component: ResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value + component.animateOut.connect { [weak self] completion in + guard let self else { + return + } + self.animateOut { + completion(Void()) + } + } + + let themeUpdated = self.environment?.theme !== sheetEnvironment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = sheetEnvironment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, sheetEnvironment.deviceMetrics.screenSize.width) - sheetEnvironment.safeInsets.left * 2.0 + } + let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + + self.component = component + self.state = state + self.environment = sheetEnvironment + + let theme = sheetEnvironment.theme.withModalBlocksBackground() + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var containerSize: CGSize + if !"".isEmpty, sheetEnvironment.isCentered { + let verticalInset: CGFloat = 44.0 + let maxSide = max(availableSize.width, availableSize.height) + let minSide = min(availableSize.width, availableSize.height) + containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0) + if let regularMetricsSize = sheetEnvironment.regularMetricsSize { + containerSize = regularMetricsSize + } + } else { + containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude) + } + + let containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0 + let clippingY: CGFloat + + self.contentView.parentState = state + let contentViewSize = self.contentView.update( + transition: transition, + component: component.content, + environment: { + environment[ChildEnvironmentType.self] + }, + containerSize: containerSize + ) + component.externalState?.contentHeight = contentViewSize.height + + if let contentView = self.contentView.view { + if contentView.superview == nil { + self.scrollContentView.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize)) + } + + let contentHeight = contentViewSize.height + let initialContentHeight = contentHeight + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) + transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) + self.topEdgeEffectView.update(content: theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + if self.topEdgeEffectView.superview == nil { + self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) + } + + if let titleItem = component.titleItem { + let titleItemView: ComponentView + if let current = self.titleItemView { + titleItemView = current + } else { + titleItemView = ComponentView() + self.titleItemView = titleItemView + } + + let titleItemSize = titleItemView.update( + transition: transition, + component: titleItem, + environment: {}, + containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0) + ) + let titleItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(36.0 - titleItemSize.height * 0.5)), size: titleItemSize) + if let view = titleItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: titleItemFrame) + } + } else if let titleItemView = self.titleItemView { + self.titleItemView = nil + titleItemView.view?.removeFromSuperview() + } + + + if let leftItem = component.leftItem { + let leftItemView: ComponentView + if let current = self.leftItemView { + leftItemView = current + } else { + leftItemView = ComponentView() + self.leftItemView = leftItemView + } + + let leftItemSize = leftItemView.update( + transition: transition, + component: leftItem, + environment: {}, + containerSize: CGSize(width: 66.0, height: 66.0) + ) + let leftItemFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: leftItemSize) + if let view = leftItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: leftItemFrame) + } + } else if let leftItemView = self.leftItemView { + self.leftItemView = nil + leftItemView.view?.removeFromSuperview() + } + + if let rightItem = component.rightItem { + let rightItemView: ComponentView + if let current = self.rightItemView { + rightItemView = current + } else { + rightItemView = ComponentView() + self.rightItemView = rightItemView + } + + let rightItemSize = rightItemView.update( + transition: transition, + component: rightItem, + environment: {}, + containerSize: CGSize(width: 66.0, height: 66.0) + ) + let rightItemFrame = CGRect(origin: CGPoint(x: containerSize.width - 16.0 - rightItemSize.width, y: 16.0), size: rightItemSize) + if let view = rightItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: rightItemFrame) + } + } else if let rightItemView = self.rightItemView { + self.rightItemView = nil + rightItemView.view?.removeFromSuperview() + } + + + clippingY = availableSize.height + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 38.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: sheetEnvironment.deviceMetrics.screenCornerRadius, bottomInset: sheetEnvironment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation { + self.animateIn() + } + + return availableSize + } + } + + 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/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 4aa7f411b7..0ca5c0ae13 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -53,6 +53,9 @@ swift_library( "//submodules/TelegramUI/Components/BatchVideoRendering", "//submodules/TelegramUI/Components/GifVideoLayer", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift index 5028d6180f..2762c21fdd 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -419,13 +419,13 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { context.setFillColor(color.withMultipliedAlpha(0.2).cgColor) - context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 21.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 30.0).cgPath) context.fillPath() context.setFillColor(color.cgColor) - let plusSize = CGSize(width: 3.5, height: 28.0) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) + let plusSize = CGSize(width: 4.0, height: 27.0) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -18.0), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -18.0), cornerRadius: plusSize.width / 2.0).cgPath) context.fillPath() context.translateBy(x: size.width / 2.0, y: size.height / 2.0) @@ -437,7 +437,7 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { let components = string.components(separatedBy: "\n") for component in components { context.saveGState() - let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(17.0), NSAttributedString.Key.foregroundColor: color]) + let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(16.0), NSAttributedString.Key.foregroundColor: color]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 3f94209d63..2ab6b15ea0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -4026,7 +4026,7 @@ public final class EmojiPagerContentComponent: Component { private func updateTopPanelSeparator(transition: ComponentTransition) { if let topPanelSeparator = self.topPanelSeparator { var offset = self.scrollView.contentOffset.y - let startOffset: CGFloat = 40.0 - self.topPanelHeight + let startOffset: CGFloat = 46.0 - self.topPanelHeight let endOffset: CGFloat = startOffset + 10.0 offset = min(max(offset, startOffset), endOffset) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index aa67230486..12ec0cfb7f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -403,7 +403,7 @@ public final class EntityKeyboardComponent: Component { if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputSettingsIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { maskContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() } @@ -417,7 +417,7 @@ public final class EntityKeyboardComponent: Component { if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Media Editor/AddImage", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { addImage() } @@ -537,7 +537,7 @@ public final class EntityKeyboardComponent: Component { if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputSettingsIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() } @@ -546,7 +546,7 @@ public final class EntityKeyboardComponent: Component { if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Media Editor/AddImage", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { addImage() } @@ -649,7 +649,7 @@ public final class EntityKeyboardComponent: Component { if let _ = deleteBackwards { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputGlobeIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return @@ -660,7 +660,7 @@ public final class EntityKeyboardComponent: Component { } else if let addImage = component.emojiContent?.inputInteractionHolder.inputInteraction?.addImage { contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Media Editor/AddImage", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { addImage() } @@ -671,7 +671,7 @@ public final class EntityKeyboardComponent: Component { if let _ = deleteBackwards { contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardBottomPanelButton( icon: "Chat/Input/Media/EntityInputClearIcon", - color: component.theme.chat.inputPanel.inputControlColor, + theme: component.theme, action: { deleteBackwards?() AudioServicesPlaySystemSound(1155) @@ -721,7 +721,8 @@ public final class EntityKeyboardComponent: Component { topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( theme: component.theme, overflowHeight: component.hiddenInputHeight, - topInset: component.externalTopPanelContainer == nil ? 6.0 : 0.0, + topInset: component.externalTopPanelContainer == nil ? 8.0 : 0.0, + height: component.externalTopPanelContainer == nil ? 40.0 : 34.0, displayBackground: component.externalTopPanelContainer != nil ? .none : component.displayTopPanelBackground )), externalTopPanelContainer: component.externalTopPanelContainer, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift index faa4d87d05..c73235e76d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelButton.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData import PagerComponent import ComponentDisplayAdapters import BundleIconComponent @@ -10,18 +11,18 @@ import AppBundle final class EntityKeyboardBottomPanelButton: Component { let icon: String - let color: UIColor + let theme: PresentationTheme let action: () -> Void let holdAction: (() -> Void)? init( icon: String, - color: UIColor, + theme: PresentationTheme, action: @escaping () -> Void, holdAction: (() -> Void)? = nil ) { self.icon = icon - self.color = color + self.theme = theme self.action = action self.holdAction = holdAction } @@ -30,7 +31,7 @@ final class EntityKeyboardBottomPanelButton: Component { if lhs.icon != rhs.icon { return false } - if lhs.color != rhs.color { + if lhs.theme !== rhs.theme { return false } if (lhs.holdAction == nil) != (rhs.holdAction == nil) { @@ -39,7 +40,9 @@ final class EntityKeyboardBottomPanelButton: Component { return true } - final class View: HighlightTrackingButton { + final class View: UIView { + private let backgroundView: GlassBackgroundView + let buttonView: HighlightTrackingButton let iconView: GlassBackgroundView.ContentImageView let tintMaskContainer: UIView @@ -48,15 +51,19 @@ final class EntityKeyboardBottomPanelButton: Component { var component: EntityKeyboardBottomPanelButton? - private var currentIsHighlighted: Bool = false { - didSet { - if self.currentIsHighlighted != oldValue { - self.updateAlpha(transition: .immediate) - } - } - } +// private var currentIsHighlighted: Bool = false { +// didSet { +// if self.currentIsHighlighted != oldValue { +// self.updateAlpha(transition: .immediate) +// } +// } +// } override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + self.buttonView = HighlightTrackingButton() + self.iconView = GlassBackgroundView.ContentImageView() self.iconView.isUserInteractionEnabled = false @@ -65,9 +72,11 @@ final class EntityKeyboardBottomPanelButton: Component { super.init(frame: frame) - self.addSubview(self.iconView) + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.iconView) + self.backgroundView.contentView.addSubview(self.buttonView) - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.buttonView.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -86,65 +95,65 @@ final class EntityKeyboardBottomPanelButton: Component { } } - override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - self.currentIsHighlighted = true - - self.holdActionTriggerred = false - - if self.component?.holdAction != nil { - self.holdActionTriggerred = true - self.component?.action() - - self.holdActionTimer?.invalidate() - let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.holdActionTimer?.invalidate() - strongSelf.component?.holdAction?() - strongSelf.beginExecuteHoldActionTimer() - }) - self.holdActionTimer = holdActionTimer - RunLoop.main.add(holdActionTimer, forMode: .common) - } - - return super.beginTracking(touch, with: event) - } +// override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { +// self.currentIsHighlighted = true +// +// self.holdActionTriggerred = false +// +// if self.component?.holdAction != nil { +// self.holdActionTriggerred = true +// self.component?.action() +// +// self.holdActionTimer?.invalidate() +// let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in +// guard let strongSelf = self else { +// return +// } +// strongSelf.holdActionTimer?.invalidate() +// strongSelf.component?.holdAction?() +// strongSelf.beginExecuteHoldActionTimer() +// }) +// self.holdActionTimer = holdActionTimer +// RunLoop.main.add(holdActionTimer, forMode: .common) +// } +// +// return super.beginTracking(touch, with: event) +// } - private func beginExecuteHoldActionTimer() { - self.holdActionTimer?.invalidate() - let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.component?.holdAction?() - }) - self.holdActionTimer = holdActionTimer - RunLoop.main.add(holdActionTimer, forMode: .common) - } +// private func beginExecuteHoldActionTimer() { +// self.holdActionTimer?.invalidate() +// let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in +// guard let strongSelf = self else { +// return +// } +// strongSelf.component?.holdAction?() +// }) +// self.holdActionTimer = holdActionTimer +// RunLoop.main.add(holdActionTimer, forMode: .common) +// } - override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { - self.currentIsHighlighted = false - - self.holdActionTimer?.invalidate() - self.holdActionTimer = nil - - super.endTracking(touch, with: event) - } - - override public func cancelTracking(with event: UIEvent?) { - self.currentIsHighlighted = false - - self.holdActionTimer?.invalidate() - self.holdActionTimer = nil - - super.cancelTracking(with: event) - } +// override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { +// self.currentIsHighlighted = false +// +// self.holdActionTimer?.invalidate() +// self.holdActionTimer = nil +// +// super.endTracking(touch, with: event) +// } +// +// override public func cancelTracking(with event: UIEvent?) { +// self.currentIsHighlighted = false +// +// self.holdActionTimer?.invalidate() +// self.holdActionTimer = nil +// +// super.cancelTracking(with: event) +// } - private func updateAlpha(transition: ComponentTransition) { - let alpha: CGFloat = self.currentIsHighlighted ? 0.6 : 1.0 - transition.setAlpha(view: self.iconView, alpha: alpha) - } +// private func updateAlpha(transition: ComponentTransition) { +// let alpha: CGFloat = self.currentIsHighlighted ? 0.6 : 1.0 +// transition.setAlpha(view: self.iconView, alpha: alpha) +// } func update(component: EntityKeyboardBottomPanelButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.icon != component.icon { @@ -153,15 +162,21 @@ final class EntityKeyboardBottomPanelButton: Component { self.component = component - self.iconView.tintColor = component.color + self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor - let size = CGSize(width: 38.0, height: 38.0) + let size = CGSize(width: 40.0, height: 40.0) if let image = self.iconView.image { let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size) self.iconView.frame = iconFrame } + let tintColor: GlassBackgroundView.TintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: true, transition: transition) + + self.buttonView.frame = CGRect(origin: .zero, size: size) + return size } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 5db313c31f..bb76bf8e46 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -8,6 +8,9 @@ import TelegramCore import ComponentDisplayAdapters import BundleIconComponent import GlassBackgroundComponent +import EdgeEffect +import LiquidLens +import TabSelectionRecognizer private final class BottomPanelIconComponent: Component { let title: String @@ -57,22 +60,15 @@ private final class BottomPanelIconComponent: Component { super.init(frame: frame) self.addSubview(self.contentView) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.component?.action() - } - } - func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.title != component.title { - let text = NSAttributedString(string: component.title, font: Font.medium(15.0), textColor: .white) + let text = NSAttributedString(string: component.title, font: Font.medium(14.0), textColor: .white) let textBounds = text.boundingRect(with: CGSize(width: 120.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) self.contentView.image = generateImage(CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height)), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -89,19 +85,9 @@ private final class BottomPanelIconComponent: Component { let textSize = self.contentView.image?.size ?? CGSize() let size = CGSize(width: textSize.width + textInset * 2.0, height: 28.0) - let color = component.theme.chat.inputPanel.inputControlColor + self.contentView.tintColor = component.theme.chat.inputPanel.panelControlColor - if self.contentView.tintColor != color { - if !transition.animation.isImmediate { - UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: { - self.contentView.tintColor = color - }, completion: nil) - } else { - self.contentView.tintColor = color - } - } - - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: (size.height - textSize.height) / 2.0 - 1.0), size: textSize)) + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: (size.height - textSize.height) / 2.0), size: textSize)) return size } @@ -163,17 +149,30 @@ final class EntityKeyboardBottomPanelComponent: Component { private var leftAccessoryButton: AccessoryButtonView? private var rightAccessoryButton: AccessoryButtonView? - private var iconViews: [AnyHashable: ComponentHostView] = [:] + private let edgeEffectView: EdgeEffectView + private let backgroundContainer: GlassBackgroundContainerView + private let liquidLensView: LiquidLensView + + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var selectedItemViews: [AnyHashable: ComponentHostView] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? + private var highlightedIconBackgroundView: UIView private var highlightedTintIconBackgroundView: UIView let tintContentMask: UIView private var component: EntityKeyboardBottomPanelComponent? + private var state: EmptyComponentState? + private var environment: PagerComponentPanelEnvironment? override init(frame: CGRect) { self.tintContentMask = UIView() + self.edgeEffectView = EdgeEffectView() + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true, customBlurRadius: 10.0) self.separatorView = UIView() @@ -182,7 +181,8 @@ final class EntityKeyboardBottomPanelComponent: Component { self.tintSeparatorView.isUserInteractionEnabled = false self.tintSeparatorView.backgroundColor = UIColor(white: 0.0, alpha: 0.7) - self.tintContentMask.addSubview(self.tintSeparatorView) + self.backgroundContainer = GlassBackgroundContainerView() + self.liquidLensView = LiquidLensView(useBackgroundContainer: false) self.highlightedIconBackgroundView = UIView() self.highlightedIconBackgroundView.isUserInteractionEnabled = false @@ -194,39 +194,103 @@ final class EntityKeyboardBottomPanelComponent: Component { self.highlightedTintIconBackgroundView.layer.cornerRadius = 10.0 self.highlightedTintIconBackgroundView.clipsToBounds = true self.highlightedTintIconBackgroundView.backgroundColor = UIColor(white: 0.0, alpha: 0.1) - - self.tintContentMask.addSubview(self.highlightedTintIconBackgroundView) - + super.init(frame: frame) - self.addSubview(self.backgroundView) - self.addSubview(self.highlightedIconBackgroundView) - self.addSubview(self.separatorView) + self.addSubview(self.edgeEffectView) + + self.addSubview(self.backgroundContainer) + self.backgroundContainer.contentView.addSubview(self.liquidLensView) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + self.tabSelectionRecognizer = tabSelectionRecognizer + self.liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func item(at point: CGPoint) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return id + } else { + let distance = abs(point.x - itemView.center.x) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let environment = self.environment else { + return + } + let location = recognizer.location(in: self.liquidLensView.contentView) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: location) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.state?.updated(transition: .immediate, isLocal: true) + } + case .ended, .cancelled: + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let item = environment.contentIcons.first(where: { $0.id == selectionGestureState.itemId }) else { + return + } + environment.navigateToContentId(item.id) + } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + default: + break + } + } + func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.theme !== component.theme { self.separatorView.backgroundColor = component.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) self.backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate) - self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor +// self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor } + + let intrinsicHeight: CGFloat = 34.0 - let height = intrinsicHeight + component.containerInsets.bottom + let height = intrinsicHeight + component.containerInsets.bottom + 20.0 let accessoryButtonOffset: CGFloat if component.containerInsets.bottom > 0.0 { - accessoryButtonOffset = 2.0 + accessoryButtonOffset = 0.0 } else { accessoryButtonOffset = -2.0 } + self.component = component + self.state = state let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + self.environment = panelEnvironment let activeContentId = panelEnvironment.activeContentId var leftAccessoryButtonComponent: AnyComponentWithIdentity? @@ -257,7 +321,7 @@ final class EntityKeyboardBottomPanelComponent: Component { environment: {}, containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) ) - let leftAccessoryButtonFrame = CGRect(origin: CGPoint(x: component.containerInsets.left + 2.0, y: accessoryButtonOffset), size: leftAccessoryButtonSize) + let leftAccessoryButtonFrame = CGRect(origin: CGPoint(x: component.containerInsets.left + 18.0, y: accessoryButtonOffset), size: leftAccessoryButtonSize) leftAccessoryButtonTransition.setFrame(view: leftAccessoryButton.view, frame: leftAccessoryButtonFrame) if let leftAccessoryButtonView = leftAccessoryButton.view.componentView as? PagerTopPanelView { if leftAccessoryButtonView.tintContentMask.superview == nil { @@ -328,7 +392,7 @@ final class EntityKeyboardBottomPanelComponent: Component { containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) ) - let rightAccessoryButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.containerInsets.right - 2.0 - rightAccessoryButtonSize.width, y: accessoryButtonOffset), size: rightAccessoryButtonSize) + let rightAccessoryButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.containerInsets.right - 18.0 - rightAccessoryButtonSize.width, y: accessoryButtonOffset), size: rightAccessoryButtonSize) rightAccessoryButtonTransition.setFrame(view: rightAccessoryButton.view, frame: rightAccessoryButtonFrame) if let rightAccessoryButtonView = rightAccessoryButton.view.componentView as? PagerTopPanelView { if rightAccessoryButtonView.tintContentMask.superview == nil { @@ -374,23 +438,37 @@ final class EntityKeyboardBottomPanelComponent: Component { var iconInfos: [AnyHashable: (size: CGSize, transition: ComponentTransition)] = [:] var iconTotalSize = CGSize() - let iconSpacing: CGFloat = 4.0 + let iconSpacing: CGFloat = 0.0 let navigateToContentId = panelEnvironment.navigateToContentId + + + //TODO:correctSize + let tabsSize = CGSize(width: 176.0, height: 40.0) + + var lensSelection: (x: CGFloat, width: CGFloat) = (0.0, 0.0) + if panelEnvironment.contentIcons.count > 1 { for icon in panelEnvironment.contentIcons { validIconIds.append(icon.id) var iconTransition = transition let iconView: ComponentHostView - if let current = self.iconViews[icon.id] { + let selectedIconView: ComponentHostView + if let current = self.itemViews[icon.id], let currentSelected = self.selectedItemViews[icon.id] { iconView = current + selectedIconView = currentSelected } else { iconTransition = .immediate iconView = ComponentHostView() - self.iconViews[icon.id] = iconView - self.addSubview(iconView) + iconView.isUserInteractionEnabled = false + selectedIconView = ComponentHostView() + selectedIconView.isUserInteractionEnabled = false + self.itemViews[icon.id] = iconView + self.selectedItemViews[icon.id] = selectedIconView + self.liquidLensView.contentView.addSubview(iconView) + self.liquidLensView.selectedContentView.addSubview(selectedIconView) } let iconSize = iconView.update( @@ -407,65 +485,90 @@ final class EntityKeyboardBottomPanelComponent: Component { containerSize: CGSize(width: 28.0, height: 28.0) ) + let _ = selectedIconView.update( + transition: iconTransition, + component: AnyComponent(BottomPanelIconComponent( + title: icon.title, + isHighlighted: icon.id == activeContentId, + theme: component.theme, + action: { + navigateToContentId(icon.id) + } + )), + environment: {}, + containerSize: CGSize(width: 28.0, height: 28.0) + ) + iconInfos[icon.id] = (size: iconSize, transition: iconTransition) if !iconTotalSize.width.isZero { - iconTotalSize.width += iconSpacing + iconTotalSize.width += iconSpacing - 8.0 } iconTotalSize.width += iconSize.width iconTotalSize.height = max(iconTotalSize.height, iconSize.height) } } - - var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0)) - if component.containerInsets.bottom > 0.0 { - nextIconOrigin.y += 3.0 - } + + var nextIconOrigin = CGPoint(x: floor((tabsSize.width - iconTotalSize.width) / 2.0), y: floor((tabsSize.height - iconTotalSize.height) / 2.0)) + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: .zero, size: availableSize)) + self.backgroundContainer.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) if panelEnvironment.contentIcons.count > 1 { for icon in panelEnvironment.contentIcons { - guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else { + guard let iconInfo = iconInfos[icon.id], let iconView = self.itemViews[icon.id], let selectedIconView = self.selectedItemViews[icon.id] else { continue } let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size) iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil) + iconInfo.transition.setFrame(view: selectedIconView, frame: iconFrame, completion: nil) - if let iconView = iconView.componentView as? BottomPanelIconComponent.View { - if iconView.tintMaskContainer.superview == nil { - self.tintContentMask.addSubview(iconView.tintMaskContainer) - } - iconInfo.transition.setFrame(view: iconView.tintMaskContainer, frame: iconFrame, completion: nil) - } +// if let iconView = iconView.componentView as? BottomPanelIconComponent.View { +// if iconView.tintMaskContainer.superview == nil { +// self.tintContentMask.addSubview(iconView.tintMaskContainer) +// } +// iconInfo.transition.setFrame(view: iconView.tintMaskContainer, frame: iconFrame, completion: nil) +// } +// if let activeContentId = activeContentId, activeContentId == icon.id { +// self.highlightedIconBackgroundView.isHidden = false +// self.highlightedTintIconBackgroundView.isHidden = false +// transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame) +// transition.setFrame(view: self.highlightedTintIconBackgroundView, frame: iconFrame) +// +// let cornerRadius: CGFloat = min(iconFrame.width, iconFrame.height) / 2.0 +// transition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: cornerRadius) +// transition.setCornerRadius(layer: self.highlightedTintIconBackgroundView.layer, cornerRadius: cornerRadius) +// } if let activeContentId = activeContentId, activeContentId == icon.id { - self.highlightedIconBackgroundView.isHidden = false - self.highlightedTintIconBackgroundView.isHidden = false - transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame) - transition.setFrame(view: self.highlightedTintIconBackgroundView, frame: iconFrame) - - let cornerRadius: CGFloat = min(iconFrame.width, iconFrame.height) / 2.0 - transition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: cornerRadius) - transition.setCornerRadius(layer: self.highlightedTintIconBackgroundView.layer, cornerRadius: cornerRadius) + lensSelection = (iconFrame.origin.x, iconFrame.width) } - - nextIconOrigin.x += iconInfo.size.width + iconSpacing + nextIconOrigin.x += iconInfo.size.width + iconSpacing - 8.0 } } - + + if let selectionGestureState = self.selectionGestureState { + lensSelection = (selectionGestureState.currentX, lensSelection.width) + } + + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 0.0), size: tabsSize)) + self.liquidLensView.update(size: tabsSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: false, transition: transition) + if activeContentId == nil { self.highlightedIconBackgroundView.isHidden = true } var removedIconViewIds: [AnyHashable] = [] - for (id, iconView) in self.iconViews { + for (id, iconView) in self.itemViews { if !validIconIds.contains(id) { removedIconViewIds.append(id) iconView.removeFromSuperview() } } + for id in removedIconViewIds { - self.iconViews.removeValue(forKey: id) + self.itemViews.removeValue(forKey: id) } transition.setFrame(view: self.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) @@ -473,8 +576,12 @@ final class EntityKeyboardBottomPanelComponent: Component { transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))) //self.backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition) + - self.component = component + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) + transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) + self.edgeEffectView.update(content: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, 50.0), transition: transition) return CGSize(width: availableSize.width, height: height) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index a7d0dfc927..83c09abbe0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -9,15 +9,18 @@ import Postbox public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let isContentInFocus: Bool + let height: CGFloat let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)> let isExpandedUpdated: (Bool, ComponentTransition) -> Void init( isContentInFocus: Bool, + height: CGFloat, visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>, isExpandedUpdated: @escaping (Bool, ComponentTransition) -> Void ) { self.isContentInFocus = isContentInFocus + self.height = height self.visibilityFractionUpdated = visibilityFractionUpdated self.isExpandedUpdated = isExpandedUpdated } @@ -26,6 +29,9 @@ public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { if lhs.isContentInFocus != rhs.isContentInFocus { return false } + if lhs.height != rhs.height { + return false + } if lhs.visibilityFractionUpdated !== rhs.visibilityFractionUpdated { return false } @@ -39,17 +45,20 @@ final class EntityKeyboardTopContainerPanelComponent: Component { let theme: PresentationTheme let overflowHeight: CGFloat let topInset: CGFloat + let height: CGFloat let displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground init( theme: PresentationTheme, overflowHeight: CGFloat, topInset: CGFloat, + height: CGFloat, displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground ) { self.theme = theme self.overflowHeight = overflowHeight self.topInset = topInset + self.height = height self.displayBackground = displayBackground } @@ -60,6 +69,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component { if lhs.overflowHeight != rhs.overflowHeight { return false } + if lhs.height != rhs.height { + return false + } if lhs.topInset != rhs.topInset { return false } @@ -105,7 +117,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { } func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let intrinsicHeight: CGFloat = 34.0 + let intrinsicHeight: CGFloat = component.height let height = intrinsicHeight + component.topInset let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value @@ -165,6 +177,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component { environment: { EntityKeyboardTopContainerPanelEnvironment( isContentInFocus: panelEnvironment.isContentInFocus, + height: intrinsicHeight, visibilityFractionUpdated: panelView.visibilityFractionUpdated, isExpandedUpdated: { [weak self] isExpanded, transition in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index d6c9379c2e..a28088ad5d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -2009,7 +2009,7 @@ public final class EntityKeyboardTopPanelComponent: Component { let panelEnvironment = environment[EntityKeyboardTopContainerPanelEnvironment.self].value self.environment = panelEnvironment - let isExpanded = availableSize.height > 34.0 + let isExpanded = availableSize.height > panelEnvironment.height let wasExpanded = self.isExpanded self.isExpanded = isExpanded diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift index 3b872bdf88..c7004ea02e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -372,7 +372,7 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { if self.backgroundHandleView.image == nil { self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) } - self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(environment.theme.overallDarkAppearance ? 0.2 : 0.07) + self.backgroundHandleView.tintColor = theme.list.itemPrimaryTextColor.withMultipliedAlpha(theme.overallDarkAppearance ? 0.2 : 0.07) let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0)) if self.backgroundHandleView.superview == nil { self.navigationBarContainer.addSubview(self.backgroundHandleView) @@ -383,13 +383,13 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, isDark: environment.theme.overallDarkAppearance, state: .generic, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", - tintColor: environment.theme.chat.inputPanel.panelControlColor + tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { [weak self] _ in @@ -413,14 +413,13 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { let containerInset: CGFloat = environment.statusBarHeight + 10.0 contentHeight += environment.safeInsets.bottom - var initialContentHeight = contentHeight let clippingY: CGFloat let titleText: String = environment.strings.Gift_ActiveAuctions_Title(Int32(self.auctionStates.count)) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -434,12 +433,12 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { transition.setFrame(view: titleView, frame: titleFrame) } - initialContentHeight = contentHeight + let initialContentHeight = contentHeight let edgeEffectHeight: CGFloat = 80.0 let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) - self.topEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + self.topEdgeEffectView.update(content: theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) if self.topEdgeEffectView.superview == nil { self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) } diff --git a/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD new file mode 100644 index 0000000000..363e3ee0c3 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerTableCellComponent", + module_name = "PeerTableCellComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramCore", + "//submodules/Components/MultilineTextComponent", + "//submodules/AccountContext", + "//submodules/AvatarNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift new file mode 100644 index 0000000000..6ff8042f6d --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/Sources/PeerTableCellComponent.swift @@ -0,0 +1,123 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import TelegramCore +import TelegramPresentationData +import MultilineTextComponent +import AvatarNode +import AccountContext + +public final class PeerTableCellComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer? + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + public static func ==(lhs: PeerTableCellComponent, rhs: PeerTableCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + public final class View: UIView { + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PeerTableCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerTableCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + var peerName: String + let avatarOverride: AvatarNodeImageOverride? + if let peerValue = component.peer { + peerName = peerValue.compactDisplayTitle + if peerName.count > 40 { + peerName = "\(peerName.prefix(40))…" + } + avatarOverride = nil + } else { + peerName = component.strings.Gift_View_HiddenName + avatarOverride = .anonymousSavedMessagesIcon(isColored: true) + } + + let avatarNaturalSize = CGSize(width: 40.0, height: 40.0) + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride) + self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + self.avatarNode.frame = avatarFrame + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + 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/TableComponent/BUILD b/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD new file mode 100644 index 0000000000..54dc0f8a77 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TableComponent", + module_name = "TableComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift new file mode 100644 index 0000000000..af189d8f10 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/TableComponent/Sources/TableComponent.swift @@ -0,0 +1,375 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import TelegramPresentationData +import MultilineTextComponent + +public final class TableComponent: CombinedComponent { + public class Item: Equatable { + public enum TitleFont { + case regular + case bold + } + + public let id: AnyHashable + public let title: String? + public let titleFont: TitleFont + public let hasBackground: Bool + public let component: AnyComponent + public let insets: UIEdgeInsets? + + public init( + id: IdType, + title: String?, + titleFont: TitleFont = .regular, + hasBackground: Bool = false, + component: AnyComponent, + insets: UIEdgeInsets? = nil + ) { + self.id = AnyHashable(id) + self.title = title + self.titleFont = titleFont + self.hasBackground = hasBackground + self.component = component + self.insets = insets + } + + public static func == (lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.titleFont != rhs.titleFont { + return false + } + if lhs.hasBackground != rhs.hasBackground { + return false + } + if lhs.component != rhs.component { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + } + + private let theme: PresentationTheme + private let items: [Item] + private let semiTransparent: Bool + + public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) { + self.theme = theme + self.items = items + self.semiTransparent = semiTransparent + } + + public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.semiTransparent != rhs.semiTransparent { + return false + } + return true + } + + public final class State: ComponentState { + var cachedLastBackgroundImage: (UIImage, PresentationTheme)? + var cachedLeftColumnImage: (UIImage, PresentationTheme)? + var cachedBorderImage: (UIImage, PresentationTheme)? + } + + public func makeState() -> State { + return State() + } + + public static var body: Body { + let leftColumnBackground = Child(Image.self) + let lastBackground = Child(Image.self) + let verticalBorder = Child(Rectangle.self) + let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let outerBorder = Child(Image.self) + + return { context in + let verticalPadding: CGFloat = 11.0 + let horizontalPadding: CGFloat = 12.0 + let borderWidth: CGFloat = 1.0 + + let borderColor: UIColor + let secondaryBackgroundColor: UIColor + if context.component.semiTransparent { + borderColor = context.component.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1) + secondaryBackgroundColor = context.component.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.05) + } else { + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor + } + + var leftColumnWidth: CGFloat = 0.0 + + var updatedTitleChildren: [Int: _UpdatedChildComponent] = [:] + var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = [] + var updatedBorderChildren: [_UpdatedChildComponent] = [] + + var i = 0 + for item in context.component.items { + guard let title = item.title else { + i += 1 + continue + } + let titleChild = titleChildren[item.id].update( + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: item.titleFont == .bold ? Font.semibold(15.0) : Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + )), + availableSize: context.availableSize, + transition: context.transition + ) + updatedTitleChildren[i] = titleChild + + if titleChild.size.width > leftColumnWidth { + leftColumnWidth = titleChild.size.width + } + i += 1 + } + + leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) + let rightColumnWidth = context.availableSize.width - leftColumnWidth + + i = 0 + var rowHeights: [Int: CGFloat] = [:] + var totalHeight: CGFloat = 0.0 + var innerTotalHeight: CGFloat = 0.0 + var innerTotalOffset: CGFloat = 0.0 + var hasRowBackground = false + var rowBackgroundIsLast = false + var hasStraightSide = false + + for item in context.component.items { + let insets: UIEdgeInsets + if let customInsets = item.insets { + insets = customInsets + } else { + insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) + } + + var titleHeight: CGFloat = 0.0 + if let titleChild = updatedTitleChildren[i] { + titleHeight = titleChild.size.height + } + + let availableValueWidth: CGFloat + if titleHeight > 0.0 { + availableValueWidth = rightColumnWidth + } else { + availableValueWidth = context.availableSize.width + } + + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append((valueChild, insets)) + + let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) + rowHeights[i] = rowHeight + totalHeight += rowHeight + if titleHeight > 0.0 { + innerTotalHeight += rowHeight + } else if i == 0 { + innerTotalOffset += rowHeight + } + + if i < context.component.items.count - 1 { + let borderChild = borderChildren[item.id].update( + component: AnyComponent(Rectangle(color: borderColor)), + availableSize: CGSize(width: context.availableSize.width, height: borderWidth), + transition: context.transition + ) + updatedBorderChildren.append(borderChild) + } + + if item.hasBackground { + if i != 0 { + rowBackgroundIsLast = true + } + hasRowBackground = true + } + if item.title == nil { + if i != 0 { + rowBackgroundIsLast = true + } + hasStraightSide = true + } + + i += 1 + } + + let borderRadius: CGFloat = 14.0 + + if hasRowBackground { + let lastBackgroundImage: UIImage + if let (currentImage, theme) = context.state.cachedLastBackgroundImage, theme === context.component.theme { + lastBackgroundImage = currentImage + } else { + lastBackgroundImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + borderRadius)) + context.clear(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0).insetBy(dx: 0.0, dy: rowBackgroundIsLast ? -borderRadius * 2.0 : 0.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setFillColor(secondaryBackgroundColor.cgColor) + context.addPath(path) + context.fillPath() + })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) + context.state.cachedLastBackgroundImage = (lastBackgroundImage, context.component.theme) + } + + let lastRowHeight: CGFloat + let position: CGFloat + if !rowBackgroundIsLast { + lastRowHeight = rowHeights[0] ?? 0 + position = lastRowHeight / 2.0 + } else { + lastRowHeight = rowHeights[i - 1] ?? 0 + position = totalHeight - lastRowHeight / 2.0 + } + let lastBackground = lastBackground.update( + component: Image(image: lastBackgroundImage), + availableSize: CGSize(width: context.availableSize.width, height: lastRowHeight), + transition: context.transition + ) + + context.add( + lastBackground + .position(CGPoint(x: context.availableSize.width / 2.0, y: position)) + ) + } + + let leftColumnImage: UIImage + if let (currentImage, theme) = context.state.cachedLeftColumnImage, theme === context.component.theme { + leftColumnImage = currentImage + } else { + leftColumnImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in + var bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height)) + context.clear(bounds) + + var offset: CGFloat = 0.0 + if hasStraightSide { + offset = rowBackgroundIsLast ? 0.0 : -borderRadius + + bounds.origin.y += offset + bounds.size.height += borderRadius + } + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setFillColor(secondaryBackgroundColor.cgColor) + context.addPath(path) + context.fillPath() + })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) + context.state.cachedLeftColumnImage = (leftColumnImage, context.component.theme) + } + + let leftColumnBackground = leftColumnBackground.update( + component: Image(image: leftColumnImage), + availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight), + transition: context.transition + ) + context.add(leftColumnBackground + .position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 2.0)) + ) + + let borderImage: UIImage + if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { + borderImage = currentImage + } else { + borderImage = generateImage(CGSize(width: borderRadius * 2.0 + 4.0, height: borderRadius * 2.0 + 4.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + + context.setBlendMode(.normal) + context.setStrokeColor(borderColor.cgColor) + context.setLineWidth(borderWidth) + context.addPath(path) + context.strokePath() + })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) + context.state.cachedBorderImage = (borderImage, context.component.theme) + } + + let outerBorder = outerBorder.update( + component: Image(image: borderImage), + availableSize: CGSize(width: context.availableSize.width, height: totalHeight), + transition: context.transition + ) + context.add(outerBorder + .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) + ) + + let verticalBorder = verticalBorder.update( + component: Rectangle(color: borderColor), + availableSize: CGSize(width: borderWidth, height: innerTotalHeight), + transition: context.transition + ) + context.add( + verticalBorder + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 2.0)) + ) + + i = 0 + var originY: CGFloat = 0.0 + for (valueChild, valueInsets) in updatedValueChildren { + let rowHeight = rowHeights[i] ?? 0.0 + + let valueFrame: CGRect + if let titleChild = updatedTitleChildren[i] { + let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) + context.add(titleChild + .position(titleFrame.center) + ) + valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) + } else { + if hasRowBackground && rowBackgroundIsLast { + valueFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - valueChild.size.width) / 2.0), y: originY + verticalPadding), size: valueChild.size) + } else { + valueFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + } + } + + context.add(valueChild + .position(valueFrame.center) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + + if i < updatedBorderChildren.count { + let borderChild = updatedBorderChildren[i] + context.add(borderChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } + + originY += rowHeight + i += 1 + } + + return CGSize(width: context.availableSize.width, height: totalHeight) + } + } +} diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index 040181f130..d25df932c3 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -60,16 +60,16 @@ private final class RestingBackgroundView: UIVisualEffectView { public final class LiquidLensView: UIView { private struct Params: Equatable { var size: CGSize - var selectionX: CGFloat - var selectionWidth: CGFloat + var selectionOrigin: CGPoint + var selectionSize: CGSize var isDark: Bool var isLifted: Bool var isCollapsed: Bool - init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { + init(size: CGSize, selectionOrigin: CGPoint, selectionSize: CGSize, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { self.size = size - self.selectionX = selectionX - self.selectionWidth = selectionWidth + self.selectionOrigin = selectionOrigin + self.selectionSize = selectionSize self.isLifted = isLifted self.isDark = isDark self.isCollapsed = isCollapsed @@ -111,12 +111,12 @@ public final class LiquidLensView: UIView { private var liftedDisplayLink: SharedDisplayLinkDriver.Link? - public var selectionX: CGFloat? { - return self.params?.selectionX + public var selectionOrigin: CGPoint? { + return self.params?.selectionOrigin } - public var selectionWidth: CGFloat? { - return self.params?.selectionWidth + public var selectionSize: CGSize? { + return self.params?.selectionSize } public init(useBackgroundContainer: Bool = true) { @@ -242,8 +242,8 @@ public final class LiquidLensView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { - let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) + public func update(size: CGSize, selectionOrigin: CGPoint, selectionSize: CGSize, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { + let params = Params(size: size, selectionOrigin: selectionOrigin, selectionSize: selectionSize, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) if self.params == params { return } @@ -369,7 +369,7 @@ public final class LiquidLensView: UIView { transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) } - let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionX, params.size.width - params.selectionWidth)), y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) + let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionOrigin.x, params.size.width - params.selectionSize.width)), y: params.selectionOrigin.y), size: CGSize(width: params.selectionSize.width, height: params.size.height)) self.updateLens(params: LensParams(baseFrame: baseLensFrame, isLifted: params.isLifted), animated: !transition.animation.isImmediate) if let legacyContentMaskView = self.legacyContentMaskView { diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD index 4166ff8413..d0bc74abb5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Markdown", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Components/BundleIconComponent", "//submodules/TextFormat", "//submodules/TelegramUI/Components/ListSectionComponent", @@ -32,6 +33,8 @@ swift_library( "//submodules/TelegramStringFormatting", "//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/TelegramUI/Components/PeerInfo/MessagePriceItem", + "//submodules/UndoUI", + "//submodules/ShareController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index be77701a7b..858ba015a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -22,6 +22,10 @@ import Markdown import TelegramStringFormatting import MessagePriceItem import ListItemComponentAdaptor +import ButtonComponent +import PlainButtonComponent +import UndoUI +import ShareController final class PostSuggestionsSettingsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -68,6 +72,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { private let subtitle = ComponentView() private let switchSection = ComponentView() private let contentSection = ComponentView() + private let linkSection = ComponentView() private var isUpdating: Bool = false @@ -166,6 +171,91 @@ final class PostSuggestionsSettingsScreenComponent: Component { } } + func dismissAllTooltips() { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + } + + func copyLink(_ link: String) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + UIPasteboard.general.string = link + + self.dismissAllTooltips() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + + func shareLink(_ link: String) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + + let context = component.context + let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: nil) + shareController.completed = { [weak controller] peerIds in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { [weak controller] peerList in + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, action == .info { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak controller] peer in + guard let peer else { + return + } + guard let navigationController = controller?.navigationController as? NavigationController else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .window(.root)) + }) + } + shareController.actionCompleted = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + controller.present(shareController, in: .window(.root)) + } + func update(component: PostSuggestionsSettingsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -429,6 +519,50 @@ final class PostSuggestionsSettingsScreenComponent: Component { if self.areSuggestionsEnabled { contentHeight += contentSectionSize.height + contentHeight += sectionSpacing + } + + let address = component.peer?.addressName ?? "" + let link = "t.me/\(address)?direct" + let fullLink = "https://\(link)" + var linkSectionItems: [AnyComponentWithIdentity] = [] + linkSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent( + LinkComponent( + theme: environment.theme, + strings: environment.strings, + link: link, + copyAction: { [weak self] in + self?.copyLink(fullLink) + }, + shareAction: { [weak self] in + self?.shareLink(fullLink) + } + ) + ))) + let linkSectionSize = self.linkSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: nil, + items: linkSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let linkSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linkSectionSize) + if let linkSectionView = self.linkSection.view { + if linkSectionView.superview == nil { + self.scrollView.addSubview(linkSectionView) + self.linkSection.parentState = state + } + transition.setFrame(view: linkSectionView, frame: linkSectionFrame) + alphaTransition.setAlpha(view: linkSectionView, alpha: self.areSuggestionsEnabled && !address.isEmpty ? 1.0 : 0.0) + } + if self.areSuggestionsEnabled && !address.isEmpty { + contentHeight += switchSectionSize.height + contentHeight += sectionSpacing } contentHeight += bottomContentInset @@ -546,3 +680,323 @@ public final class PostSuggestionsSettingsScreen: ViewControllerComponentContain super.containerLayoutUpdated(layout, transition: transition) } } + + +private final class LinkContentComponent: Component { + let theme: PresentationTheme + let link: String + + init( + theme: PresentationTheme, + link: String + ) { + self.theme = theme + self.link = link + } + + static func ==(lhs: LinkContentComponent, rhs: LinkContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class View: UIView { + private var component: LinkContentComponent? + + private let background = ComponentView() + private let link = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: LinkContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let padding: CGFloat = 10.0 + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent( + color: component.theme.list.itemInputField.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + ) + ), + environment: {}, + containerSize: availableSize + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let linkFont = Font.regular(17.0) + let linkSize = self.link.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.link, font: linkFont, textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - padding * 4.0, height: availableSize.height) + ) + let linkFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - linkSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - linkSize.height) / 2.0) - UIScreenPixel), size: linkSize) + if let linkView = self.link.view { + if linkView.superview == nil { + self.addSubview(linkView) + } + transition.setFrame(view: linkView, frame: linkFrame) + } + + 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) + } +} + +private final class LinkComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let link: String + let copyAction: () -> Void + let shareAction: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + link: String, + copyAction: @escaping () -> Void, + shareAction: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.link = link + self.copyAction = copyAction + self.shareAction = shareAction + } + + static func ==(lhs: LinkComponent, rhs: LinkComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class View: UIView { + private let linkButton = ComponentView() + private let moreButton = ComponentView() + private var copyButton = ComponentView() + private var shareButton = ComponentView() + + private var component: LinkComponent? + private weak var state: EmptyComponentState? + + private var cachedMoreImage: (UIImage, PresentationTheme)? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: LinkComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let sideInset: CGFloat = 16.0 + var contentHeight: CGFloat = sideInset + + let linkButtonSize = self.linkButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(LinkContentComponent(theme: component.theme, link: component.link)), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 52.0) + ) + let linkButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linkButtonSize) + if let linkButtonView = self.linkButton.view { + if linkButtonView.superview == nil { + self.addSubview(linkButtonView) + } + linkButtonView.frame = linkButtonFrame + } + + let moreButtonImage: UIImage + if let (image, theme) = self.cachedMoreImage, theme === component.theme { + moreButtonImage = image + } else { + moreButtonImage = actionButtonImage(color: component.theme.list.itemInputField.controlColor)! + self.cachedMoreImage = (moreButtonImage, component.theme) + } + + let moreButtonSize = self.moreButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(Image(image: moreButtonImage, contentMode: .center)), + minSize: CGSize(width: 52.0, height: 52.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: 52.0, height: 52.0) + ) + let moreButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - moreButtonSize.width, y: contentHeight), size: moreButtonSize) + if let moreButtonView = self.moreButton.view { + if moreButtonView.superview == nil { + self.addSubview(moreButtonView) + } + moreButtonView.frame = moreButtonFrame + } + + contentHeight += linkButtonSize.height + contentHeight += 10.0 + + var buttonWidth = availableSize.width - sideInset * 2.0 + buttonWidth = (buttonWidth - 10.0) / 2.0 + + let copyButtonSize = self.copyButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Copy", font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.copyAction() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 52.0) + ) + let copyButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: copyButtonSize) + if let copyButtonView = self.copyButton.view { + if copyButtonView.superview == nil { + self.addSubview(copyButtonView) + } + copyButtonView.frame = copyButtonFrame + } + + let shareButtonSize = self.shareButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Share", font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.shareAction() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 52.0) + ) + let shareButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - shareButtonSize.width, y: contentHeight), size: shareButtonSize) + if let shareButtonView = self.shareButton.view { + if shareButtonView.superview == nil { + self.addSubview(shareButtonView) + } + shareButtonView.frame = shareButtonFrame + } + + contentHeight += copyButtonSize.height + contentHeight += sideInset + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + 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) + } +} + +func stringForRemainingTime(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d", hours, minutes) + } else { + durationString = String(format: "%02d:%02d", minutes, seconds) + } + return durationString +} + +private func actionButtonImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setBlendMode(.clear) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + }) +} diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD index 1f4cce9b1f..d348f0bdf5 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD @@ -23,6 +23,8 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramCore", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift index d712e9ea04..56e8473c6c 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift @@ -14,6 +14,7 @@ import TelegramStringFormatting import BundleIconComponent import TelegramCore import TelegramPresentationData +import GlassBarButtonComponent private final class BalanceNeededSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -49,9 +50,7 @@ private final class BalanceNeededSheetContentComponent: Component { private var component: BalanceNeededSheetContentComponent? private weak var state: EmptyComponentState? - - private var cachedCloseImage: (UIImage, PresentationTheme)? - + override init(frame: CGRect) { super.init(frame: frame) } @@ -71,28 +70,27 @@ private final class BalanceNeededSheetContentComponent: Component { let sideInset: CGFloat = 16.0 - let closeImage: UIImage - if let (image, theme) = self.cachedCloseImage, theme === environment.theme { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! - self.cachedCloseImage = (closeImage, environment.theme) - } let closeButtonSize = self.closeButton.update( transition: .immediate, - component: AnyComponent(Button( - content: AnyComponent(Image(image: closeImage)), - action: { [weak self] in - guard let self, let component = self.component else { - return - } + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: environment.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in component.dismiss() } )), environment: {}, - containerSize: CGSize(width: 30.0, height: 30.0) + containerSize: CGSize(width: 40.0, height: 40.0) ) - let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width - 16.0, y: 12.0), size: closeButtonSize) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.addSubview(closeButtonView) @@ -164,10 +162,12 @@ private final class BalanceNeededSheetContentComponent: Component { contentHeight += textSize.height contentHeight += 24.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) @@ -187,9 +187,9 @@ private final class BalanceNeededSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -197,13 +197,8 @@ private final class BalanceNeededSheetContentComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height - - if environment.safeInsets.bottom.isZero { - contentHeight += 16.0 - } else { - contentHeight += environment.safeInsets.bottom + 8.0 - } - + contentHeight += buttonInsets.bottom + return CGSize(width: availableSize.width, height: contentHeight) } } @@ -307,6 +302,7 @@ private final class BalanceNeededScreenComponent: Component { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: self.sheetAnimateOut )), diff --git a/submodules/TelegramUI/Components/TabBarComponent/BUILD b/submodules/TelegramUI/Components/TabBarComponent/BUILD index e08cc9e22b..ad24d1f7d1 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/BUILD +++ b/submodules/TelegramUI/Components/TabBarComponent/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUI/Components/LiquidLens", "//submodules/AppBundle", "//submodules/SearchBarNode", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 5a03f510eb..08681dd7af 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -13,62 +13,7 @@ import TextBadgeComponent import LiquidLens import AppBundle import SearchBarNode - -private final class TabSelectionRecognizer: UIGestureRecognizer { - private var initialLocation: CGPoint? - private var currentLocation: CGPoint? - - override init(target: Any?, action: Selector?) { - super.init(target: target, action: action) - - self.delaysTouchesBegan = false - self.delaysTouchesEnded = false - } - - override func reset() { - super.reset() - - self.initialLocation = nil - } - - override func touchesBegan(_ touches: Set, with event: UIEvent) { - super.touchesBegan(touches, with: event) - - if self.initialLocation == nil { - self.initialLocation = touches.first?.location(in: self.view) - } - self.currentLocation = self.initialLocation - - self.state = .began - } - - override func touchesEnded(_ touches: Set, with event: UIEvent) { - super.touchesEnded(touches, with: event) - - self.state = .ended - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent) { - super.touchesCancelled(touches, with: event) - - self.state = .cancelled - } - - override func touchesMoved(_ touches: Set, with event: UIEvent) { - super.touchesMoved(touches, with: event) - - self.currentLocation = touches.first?.location(in: self.view) - - self.state = .changed - } - - func translation(in: UIView?) -> CGPoint { - if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation { - return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y) - } - return CGPoint() - } -} +import TabSelectionRecognizer public final class NavigationSearchView: UIView { private struct Params: Equatable { @@ -771,7 +716,7 @@ public final class TabBarComponent: Component { lensSize = CGSize(width: 48.0, height: 48.0) lensSelection = (0.0, 48.0) } - self.liquidLensView.update(size: lensSize, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) + self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) var size = tabsSize diff --git a/submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD b/submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD new file mode 100644 index 0000000000..b3d295294a --- /dev/null +++ b/submodules/TelegramUI/Components/TabSelectionRecognizer/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TabSelectionRecognizer", + module_name = "TabSelectionRecognizer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift b/submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift new file mode 100644 index 0000000000..e67f626ed8 --- /dev/null +++ b/submodules/TelegramUI/Components/TabSelectionRecognizer/Sources/TabSelectionRecognizer.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit + +public final class TabSelectionRecognizer: UIGestureRecognizer { + private var initialLocation: CGPoint? + private var currentLocation: CGPoint? + + public override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delaysTouchesBegan = false + self.delaysTouchesEnded = false + } + + public override func reset() { + super.reset() + + self.initialLocation = nil + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.initialLocation == nil { + self.initialLocation = touches.first?.location(in: self.view) + } + self.currentLocation = self.initialLocation + + self.state = .began + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.state = .ended + } + + public override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.state = .cancelled + } + + public override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + self.currentLocation = touches.first?.location(in: self.view) + + self.state = .changed + } + + public func translation(in: UIView?) -> CGPoint { + if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation { + return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y) + } + return CGPoint() + } +} diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 9622b268fe..cb5f59f863 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -106,7 +106,12 @@ public final class TextNodeWithEntities { public let textNode: TextNode private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] - private var enableLooping: Bool = true + public var enableLooping: Bool = true + public var energySavingEnableLooping: Bool = true + + private var effectiveEnableLooping: Bool { + return self.enableLooping && self.energySavingEnableLooping + } public var resetEmojiToFirstFrameAutomatically: Bool = false @@ -124,7 +129,7 @@ public final class TextNodeWithEntities { } else { isItemVisible = false } - let isVisibleForAnimations = self.enableLooping && isItemVisible && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && isItemVisible && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { itemLayer.isVisibleForAnimations = isVisibleForAnimations if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { @@ -257,7 +262,7 @@ public final class TextNodeWithEntities { } private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool, emojiOffset: CGPoint, fontSizeNorm: CGFloat) { - self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji + self.energySavingEnableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -292,7 +297,7 @@ public final class TextNodeWithEntities { self.textNode.layer.addSublayer(itemLayer) } itemLayer.enableAnimation = stickerItem.enableAnimation - let isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame) && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && self.isItemVisible(itemRect: itemFrame) && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { itemLayer.reloadAnimation() @@ -410,7 +415,12 @@ public class ImmediateTextNodeWithEntities: TextNode { public var spoilerColor: UIColor = .black public var balancedTextLayout: Bool = false - private var enableLooping: Bool = true + public var enableLooping: Bool = true + public var energySavingEnableLooping: Bool = true + + private var effectiveEnableLooping: Bool { + return self.enableLooping && self.energySavingEnableLooping + } public var arguments: TextNodeWithEntities.Arguments? @@ -423,7 +433,7 @@ public class ImmediateTextNodeWithEntities: TextNode { didSet { if !self.inlineStickerItemLayers.isEmpty && oldValue != self.visibility { for (_, itemLayer) in self.inlineStickerItemLayers { - let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && self.visibility && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { itemLayer.isVisibleForAnimations = isVisibleForAnimations if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { @@ -575,7 +585,7 @@ public class ImmediateTextNodeWithEntities: TextNode { } private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, fontSizeNorm: CGFloat) { - self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji + self.energySavingEnableLooping = context.sharedContext.energyUsageSettings.loopEmoji var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -613,7 +623,7 @@ public class ImmediateTextNodeWithEntities: TextNode { } itemLayer.enableAnimation = stickerItem.enableAnimation - let isVisibleForAnimations = self.enableLooping && self.visibility && itemLayer.enableAnimation + let isVisibleForAnimations = self.effectiveEnableLooping && self.visibility && itemLayer.enableAnimation if itemLayer.isVisibleForAnimations != isVisibleForAnimations { itemLayer.isVisibleForAnimations = isVisibleForAnimations if !isVisibleForAnimations && self.resetEmojiToFirstFrameAutomatically { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json index d4b2093823..ea33d1b785 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "backspace_24.svg", + "filename" : "backspace_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg deleted file mode 100644 index 3907ddf696..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_24.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..387f2711d1f1d92e9aeee0713ce32317531b83a5 GIT binary patch literal 4847 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 - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..729a5745525bdfc62524ba4ce510bcdb7226ce88 GIT binary patch literal 6044 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json index 92f837ea2b..f0b8b67785 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "keyboard_2444.svg", + "filename" : "settings_30 (4).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg deleted file mode 100644 index 74f7d2fe51..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/keyboard_2444.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf new file mode 100644 index 0000000000000000000000000000000000000000..38a6fcde68a892ac7c3e3ab7b27cfb64a53154ee GIT binary patch literal 7917 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 Jt&E!N{{dVRxJUp1 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json index 5a3751c0b4..94aac7db4a 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Group 1.svg", + "filename" : "trending_44.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg deleted file mode 100644 index d93a88b5d0..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/Group 1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0ce4046658fad5245f88b21f1117e61f6803629e GIT binary patch literal 8700 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json new file mode 100644 index 0000000000..d45ae2798e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dice.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf new file mode 100644 index 0000000000000000000000000000000000000000..43f73ca9aee17c5d584b100fc6cb6e322244ccd6 GIT binary patch literal 4749 zcmai2c|4SD_a{VCl0w!RNg|BF3}Z{SjIE&|y9Og8W-K$AEFrRl7W=--o(S1SWeA~+ zl3lj471@QF-;C#}=k0ml&*yjjanCj1bI$j=u5<3sxz4#o5n4J@U}*(_XdrC>z(5%w z&fWz81gfY2K^I){XiqE(j|PIY&^Is+Xp@T?G!bopbTD)o?bnqi)VL%-?a`c4_r*ROcwidIs`hIOecf7gd5l-Wg{VXg<>)VF_uN4*V3P_YQoy5-@ z_LW>IAA4Th5a2(bHawHDlTBrKfMwE>@yusy&ADoK$w$kPOK6k%Ku0jlqm~ad{*lF> z>>a#{^X9E=h#X07W8XQ9N45xT@Eo-uQ4u>NF5>PX>B_X@%_HX1Q*4>)Njw7lT^hAQ z3@Wgn$~^vUja(-<7@AE_2VC2=mICXVrAop%fnBj4fXvt+cWCo=1sr1^at`f5@%xG4 zi~K&q9MZsf=9=D?u8=V_QOLsHLQPyFfoyoJLy-@~irvTBA>0RaRr^RyW8-UQa4Ri! zqmnbI*E`-gW~|-c2h2REzAE)tD-L#2vlGj!u4NRp|9v1&57VnaM%O?l_x+161h#_J zj{r`+ba}yY0LT)q&NIMJb6W+$uo3tpOHhyzl66=#y03vv{Y7sjoL3fB9tkPF70 zwodsmoe0eTrcdN&L7a{Q=&`n{gN-GTKnVbM*wJig_KID$jDRac!Rqy-j(Mgi4cfHDEq-BKA%P zInLe%Y?#!U#Ms6AS%@dn^d&z^=*g`CUDs1LRBMj-vJHoSZP`Fwtazs=XvBUk)axb7 zq@p5Nn#22uH^7^*|Gog(|3&P#A)-JdhZ+FXGBd8fEVeAPb!*pa_t1r)tLpBjn2!=e zszXj`uxCfRN6P3V2}>M}4wr60*q`z^R}nR!-e};b>!*3@As&7&@6%~U%)z24d{M!u#k6x}NMovB+6cVq(k;zw#3Av;xU+YYDpO4^)@j`|_zt`n|MY=F=kheP z)3hDZx!y^N5o;G{_iZmvuoJVB2r&DZu$#B%dOp)TUls54|QQ*mnB4n=JQbos+ys^@TeyM=`n>Nx%Pva;kG$lp-8Wl8c5#D* z9mKBMn4rEM%WuP8Y9|hpdB`3jrXn>IrCgufuvc6T3RCzgm#Ky`=EokvV>*OmhaWvD zO2^#B+?kV-8|WG{xO$_#^sS)7qO-JoIrOCc1HKbc20E$^EgG%%5!&~(BU{hu`*)u0 zBqgXO*p+=LjVW^}4K0f={ZZysI$TPv1XoWCAqPfmv@E8dIyw4b9s8xLpi116O!pq|aF0SfuYGUQg)WWHrWd1f3#OdfZnWJu+Ay*&kTVKU zl1qzCyQFj<1hwq4vwli^uibxV%H-22LVm-m+7pEM`ng(b&e`h~jHk?W(GzDdN> zK4~k&Pnb{QE3PYkZ$0hZ90@+zjeevRk`F1q@d`1~_xuKBJz}$HGkK+Y)n;jUE^PVW zx8VhamJg0zgM=^Q0j$*MpQDV^fe!*#0t@sF62lX!r0yhcgGQgNO~?kLo6%B1zz0IL zpl`+xj1fLP<((Cs!3kF+19)788ii}cLij7sOgpwWAl~-m^h|KSI(Pb9ee*Oqhdkdr z7Up~Bc$mJ3qzF`JBpi94ORGj_BBwdWCHK)4pYoCCo{~e-N+2KicuMgX(|)s(awA*+ zThY7xZ&IFulajtA#ey?oL1`{;@RG_kvN!H_PM#QVnyy>bBOdcRehW;!nWk^;F+yzA z-)-88V15?T8vBEA-aS-`=}V68CR zvpcx!oqb)SG0OF$-oE}B)){AU&S9(fM3gDYEXuy#QbPpy{(e8(pX>WdZuj1PduOD1 z;k-E&u|fW_SUXD6-p~%)yt>J@d1#R^?3-g=L9|((ZOY$C(VrFlE{fYM9}FmaU$fTE zILX+-h#8Ej{1Cj&x+%D8oqjAy z)amuh*O_v&i;eRu#^n{IU_T~jk}_G}GN;Wcb*c=1i+l-+H+=tS*QxNP+bU9DBeDuo zS-q4-t$Pfesd;24bxU>Z@jF`&+wXqHtQ8{995$Nk557ysq{kkg0^Pe&Hpkz@~r()_FvFLoyi@I=eekbe(2r##Pg%nMi@63vSGRAvr*ox;DWpNyxM;lA|(G~%jR`M z^5*xG@72<_+II(5#dqJWCYQdgeT%j;XsYsO-`aMEqHk;{bJ)M!aH4!I6Z-m<4|?c& z{qghnMBqH{&~NU=r=&@6QZ#!Nt<};mBqqmGQ!hoRvZ;68U_hSqixdW=1a$dU!Y+(w zvIJc5=B;zIz25fB+y%nWg<(MW$}%r|fz8~{{%C;7-2U$2$7vVt3Nd)`_9)xb!ygB4 z?yCTr>KZO0du|>ooqw7r*n3>_qz88r(gM-P%gM*-UNd%bzulJKB znvEg?Kw};Mt_W!}desS#{at?Um5;w*x!|E)!@ zT{Us8IM2)OCurf(nO1Hwkww zS;Hmoz7M{h32f&CuvPL3u=xc5PnP%=dQJ{TeIqQ)q|SrvKJ9MLEZ+&}zdx0!sYjx| z*}l1@dg*&|e7-}MV$|4{ayf42GnM1~u5Zl+2vg z4{?2&rSTF@WtsgIMchmFi!*JPp43-&2&pB^RT@dT)jC}cw76<2frj)p7OX`WVbla%llvG^@cu4eF$>h;!4zoOL2Y)DN3eTRTbU z7#17jXmq;etzc7=`b7X~Agc$e=sXt^u>;RhBNt5)RLs*&;m#5dRRGHDD>aGJN<1(N zKR3CV6UH58J`buAB}sbuYbsww>%vn6+p|Q@2J>J|WF|w+-1Mrfc&ncknjkSPqZE&H^g7!hq@`Im+!D>>79&{*d~;4dy0bW$YOTrInpYhF3G!wC0W#n zsBYQXKFiwUUy6=X-%eUPv&d=7E&SZ%6iH_-%IIQ*Vv<2)L;emheYs4bS#^BcmxH)~ zO1jI$+HAll3>Wdrex@s7cv3giVf_=p#(x3U8UPz@(0g_y@6niwXG^UK<%%h=#nb#EYiv)mvP8;2hz4&4(jJ9C%oNf0vc z;uE90E{=f~7VTx-?bCwQub#<6CxY?CW6|9NkEL#N_p0%`=3{zX`RuP>Pq=|P2v2qg zid2qvC`BvB@xb#>bNF`jmLT=szO#)9SDJ5m!Lh-KiQ`;|agrZPXU(NPI!l_QWUy<( z2M}8j4Jkh5;)*Fu7{Jcw+h-pZ4I!kzm_@Js7}>X+lV zbPG`|a{=6;K`h_GX)3#5itYX`yP9EWVQSmbL*9fCm88aciZ4zzZKLS6HNR1dX)$Sl ztx`Cg`dS57%{pXuOl?HJ?jkS7`>AfrX;oK&6|UHPYY~n(RAqOjd)&*zc{@kur5jP_ zvxg#7xfJra3>{!@kTJDe!ZPW@E;&SbWv{NKQuc`S8M%yn5j-8xUO;*RrDr#Kf3Jzd;?Y>z ziu4Tm3q+tiX&6xPZ|FbrC>{FeyMaE&k+xMJ{RH#`{fm)6vjzXx{=I0(e=x!RU^2s? zz0sZsPqY)7wlsZ{bZ2N`yxd(;H??pMNDST;4a9pA&~%Q!7J_4)aC>S9Pn;vcf%cW+ ztPbXy8%q2y`JNK}0s71MEZhNy{X_Pf$pD4D20V-YCEatk*YD%+QS6056NN{);;#L9 z5F%~U02v@y2Jlxj=uaRN0+E9No!Eb4V48pTB-me=tbz>f8vHjV`#WYTdW!$e zCkIoY-IRao$-!W>VE>89gQ2v5{R@+o{|{TdCko?=_5{#F0N0`g_$(Lj0z^f%FX{Xrh5180 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f76923f6c6..ff94ad36c7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8054,64 +8054,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.presentEmojiList(references: [stickerPackReference], previewIconFile: previewIconFile) } } - - func displayDiceTooltip(dice: TelegramMediaDice) { - guard let _ = dice.value else { - return - } - self.window?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - }) - self.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() - } - return true - }) - let value: String? - let emoji = dice.emoji.strippedEmoji - switch emoji { - case "🎲": - value = self.presentationData.strings.Conversation_Dice_u1F3B2 - case "🎯": - value = self.presentationData.strings.Conversation_Dice_u1F3AF - case "🏀": - value = self.presentationData.strings.Conversation_Dice_u1F3C0 - case "⚽": - value = self.presentationData.strings.Conversation_Dice_u26BD - case "🎰": - value = self.presentationData.strings.Conversation_Dice_u1F3B0 - case "🎳": - value = self.presentationData.strings.Conversation_Dice_u1F3B3 - default: - let emojiHex = emoji.unicodeScalars.map({ String(format:"%02x", $0.value) }).joined().uppercased() - let key = "Conversation.Dice.u\(emojiHex)" - if let string = self.presentationData.strings.primaryComponent.dict[key] { - value = string - } else if let string = self.presentationData.strings.secondaryComponent?.dict[key] { - value = string - } else { - value = nil - } - } - if let value = value { - self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in - if let self, canSendMessagesToChat(self.presentationInterfaceState), action == .undo { - self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in - guard let self else { - return - } - self.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])], postpone: postpone) - }) - } - return false - }), in: .current) - } - } - func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil, repeatPeriod: Int32? = nil, postpone: Bool = false) -> [EnqueueMessage] { var defaultThreadId: Int64? var defaultReplyMessageSubject: EngineMessageReplySubject? diff --git a/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift new file mode 100644 index 0000000000..56c6b78a6c --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift @@ -0,0 +1,125 @@ +import Foundation +import AccountContext +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import TelegramPresentationData +import PresentationDataUtils +import UndoUI +import EmojiGameStakeScreen +import ChatPresentationInterfaceState +import TelegramStringFormatting + +extension ChatControllerImpl { + func presentEmojiGameStake() { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.EmojiGame()) + |> deliverOnMainQueue).start(next: { [weak self] gameInfo in + guard let self, case let .available(info) = gameInfo else { + return + } + let controller = EmojiGameStakeScreen( + context: self.context, + gameInfo: info, + completion: { [weak self] stake in + guard let self else { + return + } + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let self else { + return + } + self.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: "🎲", tonAmount: stake.value > 0 ? stake.value : nil)), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])], postpone: postpone) + }) + } + ) + self.push(controller) + }) + } + + func displayDiceTooltip(dice: TelegramMediaDice) { + guard let _ = dice.value else { + return + } + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + + let emoji = dice.emoji.strippedEmoji + + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.EmojiGame()) + |> deliverOnMainQueue).start(next: { [weak self] gameInfo in + guard let self else { + return + } + + let canSendMessages = canSendMessagesToChat(self.presentationInterfaceState) + let value: String? + var changeAction: String? + var tonAmount: Int64? + if canSendMessages, emoji == "🎲", case let .available(info) = gameInfo { + //TODO:localize + let currentStake = info.previousStake + value = "Stake: $ \(formatTonAmountText(currentStake, dateTimeFormat: self.presentationData.dateTimeFormat))" + changeAction = "change" + tonAmount = info.previousStake + } else { + switch emoji { + case "🎲": + value = self.presentationData.strings.Conversation_Dice_u1F3B2 + case "🎯": + value = self.presentationData.strings.Conversation_Dice_u1F3AF + case "🏀": + value = self.presentationData.strings.Conversation_Dice_u1F3C0 + case "⚽": + value = self.presentationData.strings.Conversation_Dice_u26BD + case "🎰": + value = self.presentationData.strings.Conversation_Dice_u1F3B0 + case "🎳": + value = self.presentationData.strings.Conversation_Dice_u1F3B3 + default: + let emojiHex = emoji.unicodeScalars.map({ String(format:"%02x", $0.value) }).joined().uppercased() + let key = "Conversation.Dice.u\(emojiHex)" + if let string = self.presentationData.strings.primaryComponent.dict[key] { + value = string + } else if let string = self.presentationData.strings.secondaryComponent?.dict[key] { + value = string + } else { + value = nil + } + } + } + if let value = value { + self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, context: self.context, text: value, action: canSendMessages ? self.presentationData.strings.Conversation_SendDice : nil, changeAction: changeAction), elevatedLayout: false, action: { [weak self] action in + if let self, canSendMessagesToChat(self.presentationInterfaceState) { + switch action { + case .undo: + self.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in + guard let self else { + return + } + self.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji, tonAmount: tonAmount)), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])], postpone: postpone) + }) + case .info: + if let _ = changeAction { + self.presentEmojiGameStake() + } + default: + break + } + } + return false + }), in: .current) + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index c537e8b80d..b855091607 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -194,7 +194,7 @@ private enum CreateChannelEntry: ItemListNodeEntry { let arguments = arguments as! CreateChannelArguments switch self { case let .channelInfo(_, _, dateTimeFormat, peer, state, avatar): - return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, editingNameCompleted: { arguments.focusOnDescription() @@ -202,11 +202,11 @@ private enum CreateChannelEntry: ItemListNodeEntry { arguments.changeProfilePhoto() }, updatingImage: avatar, tag: CreateChannelEntryTag.info) case let .setProfilePhoto(_, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .descriptionSetup(_, text, value): - return ItemListMultilineInputItem(presentationData: presentationData, text: value, placeholder: text, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in + return ItemListMultilineInputItem(presentationData: presentationData, systemStyle: .glass, text: value, placeholder: text, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, tag: CreateChannelEntryTag.description) case let .descriptionInfo(_, text): @@ -214,7 +214,7 @@ private enum CreateChannelEntry: ItemListNodeEntry { case let .usernameHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .username(theme, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(updatedText) }, action: { }) diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index d68f995c4c..05d3e8a256 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -321,7 +321,7 @@ private enum CreateGroupEntry: ItemListNodeEntry { let arguments = arguments as! CreateGroupArguments switch self { case let .groupInfo(_, _, dateTimeFormat, peer, state, avatar): - return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in + return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, mode: .editSettings, peer: peer.flatMap(EnginePeer.init), presence: nil, memberCount: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, editingNameCompleted: { arguments.done() @@ -329,13 +329,13 @@ private enum CreateGroupEntry: ItemListNodeEntry { arguments.changeProfilePhoto() }, updatingImage: avatar, tag: CreateGroupEntryTag.info) case let .setProfilePhoto(_, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) case let .usernameHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .username(theme, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(updatedText) }, action: { }) @@ -364,24 +364,24 @@ private enum CreateGroupEntry: ItemListNodeEntry { case let .usernameInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .topics(_, text): - return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in }) + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in }) case let .topicsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .autoDelete(text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { arguments.updateAutoDelete() }, tag: CreateGroupEntryTag.autoDelete) case let .autoDeleteInfo(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .member(_, _, _, dateTimeFormat, nameDisplayOrder, peer, presence): - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: presence.flatMap(EnginePeer.Presence.init), text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: presence.flatMap(EnginePeer.Presence.init), text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) case let .locationHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .location(theme, location): let imageSignal = chatMapSnapshotImage(engine: arguments.context.engine, resource: MapSnapshotMediaResource(latitude: location.latitude, longitude: location.longitude, width: 90, height: 90)) - return ItemListAddressItem(theme: theme, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil) + return ItemListAddressItem(theme: theme, systemStyle: .glass, label: "", text: location.address.replacingOccurrences(of: ", ", with: "\n"), imageSignal: imageSignal, selected: nil, sectionId: self.section, style: .blocks, action: nil) case let .changeLocation(_, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeLocation() }) case let .locationInfo(_, text): @@ -389,7 +389,7 @@ private enum CreateGroupEntry: ItemListNodeEntry { case let .venueHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .venue(_, _, venue): - return ItemListVenueItem(presentationData: presentationData, engine: arguments.context.engine, venue: venue, sectionId: self.section, style: .blocks, action: { + return ItemListVenueItem(presentationData: presentationData, systemStyle: .glass, engine: arguments.context.engine, venue: venue, sectionId: self.section, style: .blocks, action: { arguments.updateWithVenue(venue) }) } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 73d0f66575..136c5b944a 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -770,6 +770,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur var startApp: String? var text: String? var profile: Bool = false + var direct: Bool = false var referrer: String? var albumId: Int64? var collectionId: Int64? @@ -823,6 +824,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur startChannel = "" } else if queryItem.name == "profile" { profile = true + } else if queryItem.name == "direct" { + direct = true } else if queryItem.name == "startapp" { startApp = "" } @@ -918,6 +921,13 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur convertedUrl = current + "?profile" } } + if direct, let current = convertedUrl { + if current.contains("?") { + convertedUrl = current + "&direct" + } else { + convertedUrl = current + "?direct" + } + } } } else if parsedUrl.host == "hostOverride" { if let components = URLComponents(string: "/?" + query) { diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index e408d91083..24ed6920b5 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -18,7 +18,7 @@ public enum UndoOverlayContent { case swipeToReply(title: String, text: String) case actionSucceeded(title: String?, text: String, cancel: String?, destructive: Bool) case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: StickerPackItem?, context: AccountContext) - case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?) + case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?, changeAction: String?) case chatAddedToFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) case chatRemovedFromFolder(context: AccountContext, chatTitle: String, folderTitle: NSAttributedString) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 44be88e59a..0360f3cf8f 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -53,6 +53,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private let buttonNode: HighlightTrackingButtonNode private let undoButtonTextNode: ImmediateTextNode private let undoButtonNode: HighlightTrackingButtonNode + + private let changeButtonBackground: ASImageNode + private let changeButtonTextNode: ImmediateTextNode + private let panelNode: ASDisplayNode private let panelWrapperNode: ASDisplayNode private let action: (UndoOverlayAction) -> Bool @@ -103,6 +107,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.buttonNode = HighlightTrackingButtonNode() + self.changeButtonBackground = ASImageNode() + self.changeButtonBackground.displaysAsynchronously = false + self.changeButtonBackground.isUserInteractionEnabled = false + + self.changeButtonTextNode = ImmediateTextNode() + self.changeButtonTextNode.displaysAsynchronously = false + self.changeButtonTextNode.isUserInteractionEnabled = false + var displayUndo = true var undoText = presentationData.strings.Undo_Undo var undoTextColor = presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) @@ -624,7 +636,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: resource._asResource(), isVideo: isVideo), width: 80, height: 80, mode: .direct(cachePathPrefix: nil)) } } - case let .dice(dice, context, text, action): + case let .dice(dice, context, text, action, changeAction): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -633,7 +645,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural).mutableCopy() as! NSMutableAttributedString + if let range = attributedText.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: .white) { + attributedText.addAttribute(.attachment, value: icon, range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } self.textNode.attributedText = attributedText if let action = action { displayUndo = true @@ -651,6 +667,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { default: break } + + if let changeAction { + self.changeButtonTextNode.attributedText = NSAttributedString(string: changeAction, font: Font.regular(12.0), textColor: undoTextColor) + self.changeButtonBackground.image = generateStretchableFilledCircleImage(diameter: 18.0, color: undoTextColor.withMultipliedAlpha(0.1)) + } if dice.emoji == "🎰" { let slotMachineNode = SlotMachineAnimationNode(account: context.account, size: CGSize(width: 42.0, height: 42.0)) @@ -670,8 +691,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch stickerPack { case let .result(_, items, _): let item = items[Int(value)] - - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file._parse().resource), width: 120, height: 120, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file._parse().resource), width: 120, height: 120, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) default: break } @@ -1553,7 +1573,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.panelNode.backgroundColor = .clear } self.panelNode.clipsToBounds = true - self.panelNode.cornerRadius = 14.0 + self.panelNode.cornerRadius = 25.0 self.panelWrapperNode = ASDisplayNode() @@ -1625,6 +1645,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.panelWrapperNode.addSubnode(self.undoButtonTextNode) self.panelWrapperNode.addSubnode(self.undoButtonNode) } + if self.changeButtonBackground.image != nil { + self.panelWrapperNode.addSubnode(self.changeButtonBackground) + self.panelWrapperNode.addSubnode(self.changeButtonTextNode) + } self.addSubnode(self.panelNode) self.addSubnode(self.panelWrapperNode) @@ -1962,7 +1986,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } contentHeight += textSize.height - contentHeight = max(49.0, contentHeight) + contentHeight = max(50.0, contentHeight) var insets = layout.insets(options: [.input]) switch self.placementPosition { @@ -2024,6 +2048,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { transition.updateFrame(node: self.textNode, frame: textFrame) } + if self.changeButtonTextNode.supernode != nil { + let changeButtonTextSize = self.changeButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let changeButtonFrame = CGRect(origin: CGPoint(x: textFrame.maxX + 10.0, y: floorToScreenPixels(textFrame.midY - changeButtonTextSize.height * 0.5)), size: changeButtonTextSize) + transition.updateFrame(node: self.changeButtonTextNode, frame: changeButtonFrame) + transition.updateFrame(node: self.changeButtonBackground, frame: changeButtonFrame.insetBy(dx: -6.0, dy: -2.0)) + } + if let iconNode = self.iconNode { let iconSize: CGSize if let size = self.iconImageSize { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index f691900ac3..df20c4e013 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -85,6 +85,7 @@ public enum ParsedInternalPeerUrlParameter { case boost case text(String) case profile + case direct case referrer(String) case storyFolder(Int64) case giftCollection(Int64) @@ -393,6 +394,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .peer(.name(peerName), .boost) } else if queryItem.name == "profile" { return .peer(.name(peerName), .profile) + } else if queryItem.name == "direct" { + return .peer(.name(peerName), .direct) } else if queryItem.name == "startapp" { var mode: ResolvedStartAppMode = .generic if let queryItems = components.queryItems { @@ -836,6 +839,26 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) switch parameter { case .profile: return .single(.result(.peer(peer._asPeer(), .info(nil)))) + case .direct: + if case let .channel(channel) = peer, let monoforumId = channel.linkedMonoforumId { + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: monoforumId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return context.engine.peers.findChannelById(channelId: monoforumId.id._internalGetInt64Value()) + } + } + |> map { peer -> ResolveInternalUrlResult in + if let peer { + return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) + } else { + return .result(.peer(nil, .info(nil))) + } + } + } else { + return .single(.result(.peer(nil, .info(nil)))) + } case let .text(text): var textInputState: ChatTextInputState? if !text.isEmpty {