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 0000000000..387f2711d1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/backspace_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json index a4c1cddbca..55a8ba0a5e 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "keyboard_24.svg", + "filename" : "keyboard_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg deleted file mode 100644 index 6134e3e7fa..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_24.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - 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 0000000000..729a574552 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGlobeIcon.imageset/keyboard_30.pdf differ 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 0000000000..38a6fcde68 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/settings_30 (4).pdf differ 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 0000000000..0ce4046658 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelFeaturedIcon.imageset/trending_44.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json new file mode 100644 index 0000000000..f235c7eaac --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "wear_30 (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf new file mode 100644 index 0000000000..a6b1772cfd Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf differ 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 0000000000..43f73ca9ae Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Dice.imageset/dice.pdf differ 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 {