diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5a906f7fc9..7d5dae46d3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15168,3 +15168,11 @@ Error: %8$@"; "ScheduledMessages.Reminder.DeleteMany" = "Delete Reminders"; "Gift.Setup.NextDropIn" = "next drop in {m}:{s}"; + +"PrivacySettings.LoginEmailSetupInfo" = "Setup your email address for Telegram login codes."; + +"LoginEmail.Title" = "Add Email"; +"LoginEmail.Description" = "Please add your email address to keep access to your account."; + +"LoginEmail.Success.Title" = "Email added"; +"LoginEmail.Success.Text" = "Your account is now protected!"; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift index 0dc35c2220..2eb6744023 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift @@ -12,6 +12,7 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { } private let mode: Mode + private let blocking: Bool private var controllerNode: AuthorizationSequenceEmailEntryControllerNode { return self.displayNode as! AuthorizationSequenceEmailEntryControllerNode @@ -38,9 +39,10 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { public var authorization: Any? public var authorizationDelegate: Any? - public init(presentationData: PresentationData, mode: Mode, back: @escaping () -> Void) { + public init(presentationData: PresentationData, mode: Mode, blocking: Bool = false, back: @escaping () -> Void) { self.presentationData = presentationData self.mode = mode + self.blocking = blocking super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) @@ -89,6 +91,10 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { guard let layout = self.validLayout, layout.size.width < 360.0 else { return } + + if self.blocking { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + } if self.inProgress { let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift index 9080447d22..6419b614aa 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift @@ -106,7 +106,7 @@ final class AuthorizationSequenceEmailEntryControllerNode: ASDisplayNode, UIText self.noticeNode.isUserInteractionEnabled = false self.noticeNode.displaysAsynchronously = false self.noticeNode.lineSpacing = 0.1 - self.noticeNode.attributedText = NSAttributedString(string: self.strings.Login_AddEmailText, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) + self.noticeNode.attributedText = NSAttributedString(string: self.strings.LoginEmail_Description, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) if #available(iOS 13.0, *) { self.signInWithAppleButton = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, authorizationButtonStyle: theme.overallDarkAppearance ? .white : .black) @@ -218,7 +218,7 @@ final class AuthorizationSequenceEmailEntryControllerNode: ASDisplayNode, UIText let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0 - self.titleNode.attributedText = NSAttributedString(string: self.mode == .setup ? self.strings.Login_AddEmailTitle : self.strings.Login_EnterNewEmailTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.mode == .setup ? self.strings.LoginEmail_Title : self.strings.Login_EnterNewEmailTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor) let inset: CGFloat = 24.0 diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 4bee09d752..0e868d3dbe 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -210,6 +210,7 @@ public final class SheetComponent: C private let scrollView: ScrollView private let backgroundView: BackgroundView private var effectView: UIVisualEffectView? + private let clipView: BackgroundView private let contentView: ComponentView private var headerView: ComponentView? @@ -233,6 +234,7 @@ public final class SheetComponent: C self.scrollView.alwaysBounceVertical = true self.backgroundView = BackgroundView() + self.clipView = BackgroundView() self.contentView = ComponentView() @@ -398,14 +400,16 @@ public final class SheetComponent: C self.component = component self.currentHasInputHeight = sheetEnvironment.hasInputHeight - let glassInset: CGFloat = 6.0 - + var glassInset: CGFloat = 0.0 var topCornerRadius: CGFloat var bottomCornerRadius: CGFloat switch component.style { case .glass: topCornerRadius = 38.0 bottomCornerRadius = 56.0 + if availableSize.width < availableSize.height { + glassInset = 6.0 + } case .legacy: topCornerRadius = 12.0 bottomCornerRadius = 12.0 @@ -441,12 +445,7 @@ public final class SheetComponent: C containerSize = regularMetricsSize } } else { - switch component.style { - case .glass: - containerSize = CGSize(width: availableSize.width - glassInset * 2.0, height: .greatestFiniteMagnitude) - case .legacy: - containerSize = CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) - } + containerSize = CGSize(width: availableSize.width - glassInset * 2.0, height: .greatestFiniteMagnitude) } self.contentView.parentState = state @@ -463,14 +462,19 @@ public final class SheetComponent: C self.ignoreScrolling = true if let contentView = self.contentView.view { if contentView.superview == nil { - self.scrollView.addSubview(contentView) + self.scrollView.addSubview(self.clipView) + self.clipView.bottomCornersView.addSubview(contentView) } contentView.clipsToBounds = component.clipsContent contentView.layer.cornerRadius = topCornerRadius if sheetEnvironment.isCentered { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) - transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + + let clipFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize) + self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition) + transition.setFrame(view: self.clipView, frame: clipFrame, completion: nil) + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) if let effectView = self.effectView { transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) @@ -479,10 +483,16 @@ public final class SheetComponent: C } else { switch component.style { case .glass: - transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil) + let clipFrame = CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)) + self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition) + transition.setFrame(view: self.clipView, frame: clipFrame) + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil) case .legacy: - transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0)), completion: nil) + let clipFrame = CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0)) + self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition) + transition.setFrame(view: self.clipView, frame: clipFrame) + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) if let effectView = self.effectView { transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) diff --git a/submodules/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index b32f3a8205..253b07f92b 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -136,7 +136,7 @@ public final class DatePickerNode: ASDisplayNode { didSet { self.selectionNode.image = generateStretchableFilledCircleImage(diameter: 44.0, color: self.theme.selectionColor) if let size = self.validSize { - self.updateLayout(size: size) + let _ = self.updateLayout(size: size) } } } @@ -196,7 +196,7 @@ public final class DatePickerNode: ASDisplayNode { return nil } - func updateLayout(size: CGSize) { + func updateLayout(size: CGSize) -> CGFloat { var weekday = self.firstWeekday var started = false var ended = false @@ -205,6 +205,8 @@ public final class DatePickerNode: ASDisplayNode { let sideInset: CGFloat = 12.0 let cellSize: CGFloat = floor((size.width - sideInset * 2.0) / 7.0) + var maxY = 0.0 + self.selectionNode.isHidden = true for i in 0 ..< 42 { let row: Int = Int(floor(Float(i) / 7.0)) @@ -270,11 +272,13 @@ public final class DatePickerNode: ASDisplayNode { if count == self.numberOfDays { ended = true + maxY = cellFrame.maxY } } else { textNode.isHidden = true } } + return maxY } } @@ -686,8 +690,8 @@ public final class DatePickerNode: ASDisplayNode { self.transitionFraction = transitionFraction if let size = self.validLayout { let topInset: CGFloat = self.hasValueRow ? 78.0 + 44.0 : 65.0 - let containerSize = CGSize(width: size.width, height: size.height - topInset) - self.updateItems(size: containerSize, transition: .animated(duration: 0.3, curve: .spring)) + let constrainedSize = CGSize(width: min(390.0, size.width), height: size.height - topInset) + self.updateItems(size: constrainedSize, transition: .animated(duration: 0.3, curve: .spring)) } case .cancelled, .ended: let velocity = recognizer.velocity(in: self.view) @@ -715,6 +719,7 @@ public final class DatePickerNode: ASDisplayNode { } } + public var heightUpdated: ((CGFloat) -> Void)? private func updateItems(size: CGSize, update: Bool = false, transition: ContainedViewLayoutTransition) { var validIds: [Date] = [] @@ -729,7 +734,6 @@ public final class DatePickerNode: ASDisplayNode { current.minimumDate = self.state.minDate current.maximumDate = self.state.maxDate current.date = self.state.date - current.updateLayout(size: size) } else { wasAdded = true let addedItemNode = MonthNode(theme: self.theme, month: self.months[i], minimumDate: self.state.minDate, maximumDate: self.state.maxDate, date: self.state.date) @@ -737,16 +741,21 @@ public final class DatePickerNode: ASDisplayNode { self.monthNodes[self.months[i]] = addedItemNode self.contentNode.addSubnode(addedItemNode) } - if let itemNode = itemNode { + if let itemNode { let indexOffset = CGFloat(i - self.currentIndex) let itemFrame = CGRect(origin: CGPoint(x: indexOffset * size.width + self.transitionFraction * size.width, y: 0.0), size: size) + var itemHeight = size.height if wasAdded { itemNode.frame = itemFrame - itemNode.updateLayout(size: size) + itemHeight = itemNode.updateLayout(size: size) } else { transition.updateFrame(node: itemNode, frame: itemFrame) - itemNode.updateLayout(size: size) + itemHeight = itemNode.updateLayout(size: size) + } + + if i == self.currentIndex { + self.heightUpdated?(itemHeight) } } } diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index 3c62f0e67e..35f4e535c7 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -417,6 +417,7 @@ public final class LocationOptionsComponent: Component { } public final class View: HighlightTrackingButton { + private let containerView: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView private let clippingView: UIView @@ -433,6 +434,7 @@ public final class LocationOptionsComponent: Component { private var component: LocationOptionsComponent? public override init(frame: CGRect) { + self.containerView = GlassBackgroundContainerView() self.backgroundView = GlassBackgroundView() self.clippingView = UIView() self.clippingView.clipsToBounds = true @@ -441,7 +443,8 @@ public final class LocationOptionsComponent: Component { super.init(frame: frame) - self.addSubview(self.backgroundView) + self.addSubview(self.containerView) + self.containerView.contentView.addSubview(self.backgroundView) self.addSubview(self.clippingView) self.clippingView.addSubview(self.collapsedContainerView) self.clippingView.addSubview(self.expandedContainerView) @@ -566,7 +569,7 @@ public final class LocationOptionsComponent: Component { let collapsedFrame = CGRect(origin: CGPoint(x: expandedSize.width - normalSize.width, y: expandedSize.height - normalSize.height), size: normalSize) let effectiveBackgroundFrame = component.showMapModes ? expandedFrame : collapsedFrame - self.backgroundView.update(size: effectiveBackgroundFrame.size, cornerRadius: cornerRadius, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.rootController.navigationBar.glassBarButtonBackgroundColor), transition: transition) + self.backgroundView.update(size: effectiveBackgroundFrame.size, cornerRadius: cornerRadius, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.rootController.navigationBar.glassBarButtonBackgroundColor), isInteractive: true, transition: transition) transition.setFrame(view: self.backgroundView, frame: effectiveBackgroundFrame) transition.setFrame(view: self.clippingView, frame: effectiveBackgroundFrame) @@ -635,6 +638,9 @@ public final class LocationOptionsComponent: Component { transition.setAlpha(view: self.collapsedContainerView, alpha: component.showMapModes ? 0.0 : 1.0) transition.setAlpha(view: self.expandedContainerView, alpha: component.showMapModes ? 1.0 : 0.0) + self.containerView.update(size: expandedSize, isDark: component.theme.overallDarkAppearance, transition: transition) + transition.setFrame(view: self.containerView, frame: CGRect(origin: .zero, size: expandedSize)) + return expandedSize } diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index 5fcd51e3c5..072865eccd 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -55,6 +55,14 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate { var customHitTest: ((CGPoint) -> Bool)? private var allowSelectionChanges = true + var onTouch: (() -> Void)? + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + self.onTouch?() + } + @objc override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let customHitTest = self.customHitTest, customHitTest(gestureRecognizer.location(in: self)) { return false @@ -232,6 +240,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { var returnedToUserLocation = true var ignoreRegionChanges = false var isDragging = false + var onTouch: (() -> Void)? var beganInteractiveDragging: (() -> Void)? var endedInteractiveDragging: ((CLLocationCoordinate2D) -> Void)? var disableHorizontalTransitionGesture = false @@ -353,6 +362,12 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { return false } + mapView.onTouch = { [weak self] in + guard let self else { + return + } + self.onTouch?() + } self.view.addSubview(self.pickerAnnotationContainerView) } diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 68129cfaf6..1f10085c7b 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -300,7 +300,9 @@ public final class LocationPickerController: ViewController, AttachmentContainab navigationBar.setContentNode(nil, animated: true) self.controllerNode.deactivateSearch() - self.updateTabBarVisibility(true, .animated(duration: 0.4, curve: .spring)) + if !self.controllerNode.isPickingLocation { + self.updateTabBarVisibility(true, .animated(duration: 0.4, curve: .spring)) + } }, dismissInput: { [weak self] in guard let self else { return diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 176b4b2f7a..f07f056ca2 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -1007,6 +1007,19 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } } + self.headerNode.mapNode.onTouch = { [weak self] in + guard let self else { + return + } + if self.state.displayingMapModeOptions { + self.updateState { state in + var state = state + state.displayingMapModeOptions = false + return state + } + } + } + self.headerNode.mapNode.beganInteractiveDragging = { [weak self] in guard let self, let controller = self.controller else { return @@ -1133,11 +1146,16 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM let searchContainerNode = LocationSearchContainerNode(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, coordinate: coordinate, interaction: self.interaction, story: self.source == .story) self.insertSubnode(searchContainerNode, belowSubnode: navigationBar) self.searchContainerNode = searchContainerNode - + searchContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + if let sendButtonView = self.sendButton?.view { + self.view.insertSubview(sendButtonView, belowSubview: searchContainerNode.view) + self.view.insertSubview(self.bottomEdgeEffectView, belowSubview: sendButtonView) + } + return searchContainerNode.isSearching } @@ -1188,7 +1206,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - emptyTextSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - emptyTextSize.height - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom) / 2.0)), size: emptyTextSize)) } - private var isPickingLocation: Bool { + var isPickingLocation: Bool { return (self.state.selectedLocation.isCustom || self.state.forceSelection) && !self.state.searchingVenuesAround } diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift index f3ca8873ed..aef7f092ae 100644 --- a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -148,12 +148,12 @@ final class LocationSearchContainerNode: ASDisplayNode { self.emptyResultsTitleNode = ImmediateTextNode() self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) self.emptyResultsTitleNode.textAlignment = .center - self.emptyResultsTitleNode.isHidden = true + self.emptyResultsTitleNode.alpha = 0.0 self.emptyResultsTextNode = ImmediateTextNode() self.emptyResultsTextNode.maximumNumberOfLines = 0 self.emptyResultsTextNode.textAlignment = .center - self.emptyResultsTextNode.isHidden = true + self.emptyResultsTextNode.alpha = 0.0 super.init() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6f9a8c13cf..3f8a3d13fe 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -318,6 +318,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att fileprivate var scrolledToTop = true fileprivate var scrolledExactlyToTop = true + fileprivate var isSwitchingAssetGroup = false private var didSetReady = false private let _ready = Promise() @@ -2365,6 +2366,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att guard let self else { return } + self.controllerNode.isSwitchingAssetGroup = true self.controllerNode.resetOnUpdate = true if collection.assetCollectionSubtype == .smartAlbumUserLibrary { self.selectedCollectionValue = nil @@ -2375,6 +2377,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } self.scrollToTop?() dismissImpl?() + + Queue.mainQueue().after(0.1) { + self.controllerNode.isSwitchingAssetGroup = false + } } ) @@ -2539,7 +2545,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att moreIsVisible = count > 0 } - let useGlassButtons = isBack || !self.controllerNode.scrolledToTop + let useGlassButtons = (isBack || !self.controllerNode.scrolledToTop) && !self.controllerNode.isSwitchingAssetGroup let barButtonSideInset: CGFloat = 16.0 let barButtonSize = CGSize(width: 40.0, height: 40.0) diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 6ec1448740..9deadca77c 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -80,27 +80,27 @@ private enum LogoutOptionsEntry: ItemListNodeEntry, Equatable { case let .alternativeHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .addAccount(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.addAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.addAccount, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.addAccount() }) case let .setPasscode(_, title, text): - return ItemListDisclosureItem(presentationData: presentationData, icon: PresentationResourcesSettings.setPasscode, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.setPasscode, 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.clearCache, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesSettings.clearCache, title: title, label: text, labelStyle: .multilineDetailText, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.clearCache() }) 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 .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 .logout(_, 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.logout() }) case let .logoutInfo(_, title): diff --git a/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift b/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift index 8443fe1e20..2befa317e3 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/LoginEmailSetupController.swift @@ -32,7 +32,7 @@ final class LoginEmailSetupDelegate: NSObject, ASAuthorizationControllerDelegate } } -public func loginEmailSetupController(context: AccountContext, emailPattern: String?, navigationController: NavigationController?, completion: @escaping () -> Void) -> ViewController { +public func loginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, navigationController: NavigationController?, completion: @escaping () -> Void) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var dismissEmailControllerImpl: (() -> Void)? var presentControllerImpl: ((ViewController) -> Void)? @@ -57,7 +57,7 @@ public func loginEmailSetupController(context: AccountContext, emailPattern: Str navigationController.setViewControllers(controllers, animated: true) Queue.mainQueue().after(0.5, { - navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .emoji(name: "IntroLetter", text: presentationData.strings.Login_EmailChanged), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.LoginEmail_Success_Title, text: presentationData.strings.LoginEmail_Success_Text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) }) @@ -65,7 +65,7 @@ public func loginEmailSetupController(context: AccountContext, emailPattern: Str } } - let emailController = AuthorizationSequenceEmailEntryController(presentationData: presentationData, mode: emailPattern != nil ? .change : .setup, back: { + let emailController = AuthorizationSequenceEmailEntryController(presentationData: presentationData, mode: emailPattern != nil ? .change : .setup, blocking: blocking, back: { dismissEmailControllerImpl?() }) emailController.proceedWithEmail = { [weak emailController] email in diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 14ec641261..39bbd07164 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -620,7 +620,8 @@ private func privacyAndSecurityControllerEntries( canAutoarchive: Bool, isPremiumDisabled: Bool, isPremium: Bool, - loginEmail: String? + loginEmail: String?, + accountPeer: EnginePeer? ) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] @@ -672,9 +673,17 @@ private func privacyAndSecurityControllerEntries( } entries.append(.messageAutoremoveInfo(presentationData.theme, presentationData.strings.Settings_AutoDeleteInfo)) - if loginEmail != nil { + var showLoginEmail = false + if let _ = loginEmail { + showLoginEmail = true + } else if case let .user(user) = accountPeer, let phone = user.phone, phone.hasPrefix("7") { + showLoginEmail = true + } else if presentationData.strings.baseLanguageCode == "ru" { + showLoginEmail = true + } + if showLoginEmail { entries.append(.loginEmail(presentationData.theme, presentationData.strings.PrivacySettings_LoginEmail, loginEmail)) - entries.append(.loginEmailInfo(presentationData.theme, presentationData.strings.PrivacySettings_LoginEmailInfo)) + entries.append(.loginEmailInfo(presentationData.theme, loginEmail == nil ? presentationData.strings.PrivacySettings_LoginEmailSetupInfo : presentationData.strings.PrivacySettings_LoginEmailInfo)) } entries.append(.privacyHeader(presentationData.theme, presentationData.strings.PrivacySettings_PrivacyTitle)) @@ -1461,7 +1470,7 @@ public func privacyAndSecurityController( let isPremium = accountPeer?.isPremium ?? false let isPremiumDisabled = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }).isPremiumDisabled - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium, loginEmail: loginEmail), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium, loginEmail: loginEmail, accountPeer: accountPeer), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -1531,7 +1540,7 @@ public func privacyAndSecurityController( } setupEmailImpl = { emailPattern in - let controller = loginEmailSetupController(context: context, emailPattern: emailPattern, navigationController: getNavigationControllerImpl?(), completion: { + let controller = loginEmailSetupController(context: context, blocking: false, emailPattern: emailPattern, navigationController: getNavigationControllerImpl?(), completion: { updatedTwoStepAuthData?() }) pushControllerImpl?(controller, true) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 58bfb14a9e..2f84d8421e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -3519,7 +3519,7 @@ final class VideoChatScreenComponent: Component { context: call.accountContext, theme: environment.theme, strings: environment.strings, - style: .glass, + style: .videoChat, placeholder: .plain(environment.strings.VoiceChat_MessagePlaceholder), sendPaidMessageStars: nil, maxLength: characterLimit, diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index fe6816a6f8..da41fcca59 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -183,7 +183,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, flags |= Int32(1 << 15) if let scheduleInfoAttribute { - effectiveScheduleRepeatPeriod = scheduleInfoAttribute.repeatPeriod + effectiveScheduleRepeatPeriod = scheduleInfoAttribute.repeatPeriod ?? 0 flags |= Int32(1 << 18) } } diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index 7e1b7cd1cf..82a8ff78b6 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -23,15 +23,24 @@ private final class ChatScheduleTimeSheetContentComponent: Component { let context: AccountContext let mode: ChatScheduleTimeScreen.Mode + let currentTime: Int32? + let currentRepeatPeriod: Int32? + let minimalTime: Int32? let dismiss: () -> Void init( context: AccountContext, mode: ChatScheduleTimeScreen.Mode, + currentTime: Int32?, + currentRepeatPeriod: Int32?, + minimalTime: Int32?, dismiss: @escaping () -> Void ) { self.context = context self.mode = mode + self.currentTime = currentTime + self.currentRepeatPeriod = currentRepeatPeriod + self.minimalTime = minimalTime self.dismiss = dismiss } @@ -43,6 +52,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { private let cancel = ComponentView() private let title = ComponentView() private let button = ComponentView() + private let onlineButton = ComponentView() private var datePicker: DatePickerNode? @@ -56,13 +66,17 @@ private final class ChatScheduleTimeSheetContentComponent: Component { private let repeatTitle = ComponentView() private let repeatValue = ComponentView() - private let timePicker = ComponentView() - private let repeatPicker = ComponentView() + private var timePicker = ComponentView() + private var repeatPicker = ComponentView() private var component: ChatScheduleTimeSheetContentComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? + private var isUpdating = false + + private var monthHeight: CGFloat? + private var date: Date? private var minDate: Date? private var maxDate: Date? @@ -81,16 +95,13 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.dateFormatter.timeZone = TimeZone.current super.init(frame: frame) - - self.layer.addSublayer(self.topSeparator) - self.layer.addSublayer(self.bottomSeparator) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func updateMinimumDate(currentTime: Int32? = nil) { + private func updateMinimumDate(currentTime: Int32? = nil, minimalTime: Int32? = nil) { let timeZone = TimeZone(secondsFromGMT: 0)! var calendar = Calendar(identifier: .gregorian) calendar.timeZone = timeZone @@ -107,9 +118,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate { - let minimalTime: Double = 0 //self.minimalTime.flatMap(Double.init) ?? 0.0 - self.minDate = max(next1MinDate, Date(timeIntervalSince1970: minimalTime)) - if let currentTime = currentTime, Double(currentTime) > max(currentDate.timeIntervalSince1970, minimalTime) { + let minimalTimeValue = minimalTime.flatMap(Double.init) ?? 0.0 + self.minDate = max(next1MinDate, Date(timeIntervalSince1970: minimalTimeValue)) + if let currentTime = currentTime, Double(currentTime) > max(currentDate.timeIntervalSince1970, minimalTimeValue) { self.date = Date(timeIntervalSince1970: Double(currentTime)) } else { self.date = next5MinDate @@ -118,11 +129,16 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } func update(component: ChatScheduleTimeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } let environment = environment[EnvironmentType.self].value self.environment = environment if self.component == nil { - self.updateMinimumDate(currentTime: nil) + self.updateMinimumDate(currentTime: component.currentTime, minimalTime: component.minimalTime) + self.repeatPeriod = component.currentRepeatPeriod } self.component = component @@ -211,6 +227,21 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.addSubview(datePicker.view) self.datePicker = datePicker } + datePicker.heightUpdated = { [weak self] height in + guard let self else { + return + } + var transition = ComponentTransition.spring(duration: 0.3) + if self.monthHeight == nil { + transition = .immediate + } + if height != self.monthHeight { + self.monthHeight = height + if !self.isUpdating { + self.state?.updated(transition: transition) + } + } + } datePicker.displayDateSelection = true if let minDate = self.minDate { @@ -224,15 +255,23 @@ private final class ChatScheduleTimeSheetContentComponent: Component { let constrainedWidth = min(390.0, availableSize.width) let cellSize = floor((constrainedWidth - 12.0 * 2.0) / 7.0) - let pickerHeight = 59.0 + cellSize * 5.0 + let pickerHeight = 59.0 + cellSize * 6.0 let datePickerSize = CGSize(width: availableSize.width - 22.0, height: pickerHeight) datePicker.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - datePickerSize.width) / 2.0), y: contentHeight), size: datePickerSize) datePicker.updateLayout(size: datePickerSize, transition: .immediate) - contentHeight += pickerHeight - self.topSeparator.frame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel)) + if let monthHeight = self.monthHeight { + contentHeight += monthHeight + 79.0 + } else { + contentHeight += pickerHeight + } + + transition.setFrame(layer: self.topSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) self.topSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor + if self.topSeparator.superlayer == nil { + self.layer.addSublayer(self.topSeparator) + } let timeTitleSize = self.timeTitle.update( transition: transition, @@ -250,7 +289,6 @@ private final class ChatScheduleTimeSheetContentComponent: Component { transition.setFrame(view: timeTitleView, frame: timeTitleFrame) } - let date = self.date ?? Date() var t: time_t = Int(date.timeIntervalSince1970) @@ -296,43 +334,13 @@ private final class ChatScheduleTimeSheetContentComponent: Component { transition.setFrame(view: timeValueView, frame: timeValueFrame) } - if self.isPickingTime { - let timePickerSize = self.timePicker.update( - transition: transition, - component: AnyComponent( - MenuComponent(component: AnyComponent(TimeMenuComponent( - value: self.date ?? Date(), - valueUpdated: { [weak self] value in - guard let self else { - return - } - self.date = value - self.state?.updated() - } - ))) - ), - environment: {}, - containerSize: availableSize - ) - let timePickerFrame = CGRect(origin: CGPoint(x: timeValueFrame.maxX - timePickerSize.width + 80.0, y: timeValueFrame.minY - 20.0 - timePickerSize.height + 80.0), size: timePickerSize) - if let timePickerView = self.timePicker.view as? MenuComponent.View { - if timePickerView.superview == nil { - self.addSubview(timePickerView) - - timePickerView.animateIn() - } - transition.setFrame(view: timePickerView, frame: timePickerFrame) - } - } else if let timePicker = self.timePicker.view as? MenuComponent.View, timePicker.superview != nil { - timePicker.animateOut(completion: { - timePicker.removeFromSuperview() - }) - } - contentHeight += 56.0 - self.bottomSeparator.frame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel)) + transition.setFrame(layer: self.bottomSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) self.bottomSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor + if self.bottomSeparator.superlayer == nil { + self.layer.addSublayer(self.bottomSeparator) + } let repeatTitleSize = self.repeatTitle.update( transition: transition, @@ -414,70 +422,6 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } transition.setFrame(view: repeatValueView, frame: repeatValueFrame) } - - if self.isPickingRepeatPeriod { - let repeatPickerSize = self.repeatPicker.update( - transition: transition, - component: AnyComponent( - MenuComponent(component: AnyComponent(RepeatMenuComponent( - value: self.repeatPeriod, - valueUpdated: { [weak self] value in - guard let self, let component = self.component, let environment = self.environment else { - return - } - self.isPickingRepeatPeriod = false - if component.context.isPremium { - self.repeatPeriod = value - } else { - let toastController = UndoOverlayController( - presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, - content: .premiumPaywall( - title: "Premium Required", - text: "Subscribe to **Telegram Premium** to schedule repeating messages.", - customUndoText: nil, - timeout: nil, - linkAction: nil - ), - elevatedLayout: true, - action: { [weak environment] action in - if case .info = action { - var replaceImpl: ((ViewController) -> Void)? - let controller = component.context.sharedContext.makePremiumDemoController(context: component.context, subject: .colors, forceDark: false, action: { - let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .nameColor, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - environment?.controller()?.push(controller) - } - return true - } - ) - environment.controller()?.present(toastController, in: .current) - } - self.state?.updated() - } - ))) - ), - environment: {}, - containerSize: availableSize - ) - let repeatPickerFrame = CGRect(origin: CGPoint(x: repeatValueFrame.maxX - repeatPickerSize.width + 80.0, y: repeatValueFrame.minY - 20.0 - repeatPickerSize.height + 80.0), size: repeatPickerSize) - if let repeatPickerView = self.repeatPicker.view as? MenuComponent.View { - if repeatPickerView.superview == nil { - self.addSubview(repeatPickerView) - - repeatPickerView.animateIn() - } - transition.setFrame(view: repeatPickerView, frame: repeatPickerFrame) - } - } else if let repeatPicker = self.repeatPicker.view as? MenuComponent.View, repeatPicker.superview != nil { - repeatPicker.animateOut(completion: { - repeatPicker.removeFromSuperview() - }) - } - contentHeight += 70.0 let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: environment.dateTimeFormat) @@ -541,10 +485,177 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } contentHeight += buttonSize.height + if case .scheduledMessages(true) = component.mode { + contentHeight += 8.0 + + let buttonSize = self.onlineButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1), + foreground: environment.theme.list.itemCheckColors.fillColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8), + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: environment.strings.Conversation_ScheduleMessage_SendWhenOnline, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.fillColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() as? ChatScheduleTimeScreen else { + return + } + controller.completion( + ChatScheduleTimeScreen.Result( + time: scheduleWhenOnlineTimestamp, + repeatPeriod: nil + ) + ) + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.onlineButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + } + let bottomPanelPadding: CGFloat = 15.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding contentHeight += bottomInset + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + + if self.isPickingTime { + let _ = self.timePicker.update( + transition: transition, + component: AnyComponent( + MenuComponent( + theme: environment.theme, + sourceFrame: timeValueFrame, + component: AnyComponent(TimeMenuComponent( + value: self.date ?? Date(), + valueUpdated: { [weak self] value in + guard let self else { + return + } + self.date = value + self.state?.updated() + } + )), + dismiss: { [weak self] in + guard let self else { + return + } + self.isPickingTime = false + self.state?.updated() + } + ) + ), + environment: { + }, + containerSize: contentSize + ) + let timePickerFrame = CGRect(origin: .zero, size: contentSize) + if let timePickerView = self.timePicker.view as? MenuComponent.View { + if timePickerView.superview == nil { + self.addSubview(timePickerView) + + timePickerView.animateIn() + } + transition.setFrame(view: timePickerView, frame: timePickerFrame) + } + } else if let timePicker = self.timePicker.view as? MenuComponent.View, timePicker.superview != nil { + self.timePicker = ComponentView() + timePicker.animateOut(completion: { + timePicker.removeFromSuperview() + }) + } + + if self.isPickingRepeatPeriod { + let _ = self.repeatPicker.update( + transition: transition, + component: AnyComponent( + MenuComponent( + theme: environment.theme, + sourceFrame: repeatValueFrame, + component: AnyComponent(RepeatMenuComponent( + value: self.repeatPeriod, + valueUpdated: { [weak self] value in + guard let self, let component = self.component, let environment = self.environment else { + return + } + self.isPickingRepeatPeriod = false + if component.context.isPremium { + self.repeatPeriod = value + } else { + let toastController = UndoOverlayController( + presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, + content: .premiumPaywall( + title: "Premium Required", + text: "Subscribe to **Telegram Premium** to schedule repeating messages.", + customUndoText: nil, + timeout: nil, + linkAction: nil + ), + elevatedLayout: true, + action: { [weak environment] action in + if case .info = action { + var replaceImpl: ((ViewController) -> Void)? + let controller = component.context.sharedContext.makePremiumDemoController(context: component.context, subject: .colors, forceDark: false, action: { + let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .nameColor, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + environment?.controller()?.push(controller) + } + return true + } + ) + environment.controller()?.present(toastController, in: .current) + } + self.state?.updated() + } + )), + dismiss: { [weak self] in + guard let self else { + return + } + self.isPickingRepeatPeriod = false + self.state?.updated() + } + ) + ), + environment: { + }, + containerSize: contentSize + ) + let repeatPickerFrame = CGRect(origin: .zero, size: contentSize) + if let repeatPickerView = self.repeatPicker.view as? MenuComponent.View { + if repeatPickerView.superview == nil { + self.addSubview(repeatPickerView) + + repeatPickerView.animateIn() + } + transition.setFrame(view: repeatPickerView, frame: repeatPickerFrame) + } + } else if let repeatPicker = self.repeatPicker.view as? MenuComponent.View, repeatPicker.superview != nil { + self.repeatPicker = ComponentView() + repeatPicker.animateOut(completion: { + repeatPicker.removeFromSuperview() + }) + } + // if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { // let sideInset: CGFloat = 0.0 @@ -570,7 +681,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { // } - return CGSize(width: availableSize.width, height: contentHeight) + return contentSize } } @@ -588,13 +699,22 @@ private final class ChatScheduleTimeScreenComponent: Component { let context: AccountContext let mode: ChatScheduleTimeScreen.Mode + let currentTime: Int32? + let currentRepeatPeriod: Int32? + let minimalTime: Int32? init( context: AccountContext, - mode: ChatScheduleTimeScreen.Mode + mode: ChatScheduleTimeScreen.Mode, + currentTime: Int32?, + currentRepeatPeriod: Int32?, + minimalTime: Int32? ) { self.context = context self.mode = mode + self.currentTime = currentTime + self.currentRepeatPeriod = currentRepeatPeriod + self.minimalTime = minimalTime } static func ==(lhs: ChatScheduleTimeScreenComponent, rhs: ChatScheduleTimeScreenComponent) -> Bool { @@ -604,6 +724,15 @@ private final class ChatScheduleTimeScreenComponent: Component { if lhs.mode != rhs.mode { return false } + if lhs.currentTime != rhs.currentTime { + return false + } + if lhs.currentRepeatPeriod != rhs.currentRepeatPeriod { + return false + } + if lhs.minimalTime != rhs.minimalTime { + return false + } return true } @@ -650,6 +779,9 @@ private final class ChatScheduleTimeScreenComponent: Component { content: AnyComponent(ChatScheduleTimeSheetContentComponent( context: component.context, mode: component.mode, + currentTime: component.currentTime, + currentRepeatPeriod: component.currentRepeatPeriod, + minimalTime: component.minimalTime, dismiss: { [weak self] in guard let self else { return @@ -663,6 +795,7 @@ private final class ChatScheduleTimeScreenComponent: Component { )), style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, animateOut: self.sheetAnimateOut )), environment: { @@ -707,14 +840,21 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { public init( context: AccountContext, mode: Mode, + currentTime: Int32?, + currentRepeatPeriod: Int32?, + minimalTime: Int32?, + isDark: Bool, completion: @escaping (Result) -> Void ) { self.completion = completion super.init(context: context, component: ChatScheduleTimeScreenComponent( context: context, - mode: mode - ), navigationBarAppearance: .none) + mode: mode, + currentTime: currentTime, + currentRepeatPeriod: currentRepeatPeriod, + minimalTime: minimalTime + ), navigationBarAppearance: .none, theme: isDark ? .dark : .default) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal @@ -867,15 +1007,30 @@ private final class ButtonContentComponent: Component { private final class MenuComponent: Component { - public let component: AnyComponent + let theme: PresentationTheme + let sourceFrame: CGRect + let component: AnyComponent + let dismiss: () -> Void - public init( - component: AnyComponent + init( + theme: PresentationTheme, + sourceFrame: CGRect, + component: AnyComponent, + dismiss: @escaping () -> Void ) { + self.theme = theme + self.sourceFrame = sourceFrame self.component = component + self.dismiss = dismiss } public static func ==(lhs: MenuComponent, rhs: MenuComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sourceFrame != rhs.sourceFrame { + return false + } if lhs.component != rhs.component { return false } @@ -883,62 +1038,67 @@ private final class MenuComponent: Component { } public final class View: UIView { + private let buttonView: UIButton + private let containerView: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView private var componentView: ComponentView? private var component: MenuComponent? public override init(frame: CGRect) { + self.buttonView = UIButton() + self.containerView = GlassBackgroundContainerView() self.backgroundView = GlassBackgroundView() super.init(frame: frame) - self.addSubview(self.backgroundView) + self.addSubview(self.buttonView) + self.addSubview(self.containerView) + self.containerView.contentView.addSubview(self.backgroundView) + + self.buttonView.addTarget(self, action: #selector(self.tapped), for: .touchUpInside) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func animateIn() { - let duration: Double = 0.35 - - self.layer.removeAllAnimations() - self.alpha = 0.0 - self.transform = CGAffineTransform(scaleX: 0.001, y: 0.001) - self.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0) - - UIView.animate( - withDuration: duration, - delay: 0.0, - usingSpringWithDamping: 0.75, - initialSpringVelocity: 0.6, - options: [.curveEaseOut], - animations: { - self.transform = .identity - self.alpha = 1.0 - }, - completion: nil - ) + @objc func tapped() { + if let component = self.component { + component.dismiss() + } } - public func animateOut(duration: TimeInterval = 0.15, completion: (() -> Void)? = nil) { - self.layer.removeAllAnimations() - self.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0) + func animateIn() { + guard let component = self.component else { + return + } + let transition = ComponentTransition.spring(duration: 0.3) + transition.animatePosition(view: self.backgroundView, from: component.sourceFrame.center, to: self.backgroundView.center) + transition.animateScale(view: self.backgroundView, from: 0.2, to: 1.0) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + public func animateOut(completion: (() -> Void)? = nil) { + guard let component = self.component else { + return + } - UIView.animate( - withDuration: duration, - delay: 0.0, - options: [.curveEaseInOut], - animations: { - self.transform = CGAffineTransform(scaleX: 0.001, y: 0.001) - }, - completion: { _ in - completion?() - } - ) + let transition = ComponentTransition.spring(duration: 0.3) + transition.setPosition(view: self.backgroundView, position: component.sourceFrame.center) + transition.setScale(view: self.backgroundView, scale: 0.2) + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion?() + }) } + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundView.frame.contains(point) && self.buttonView.frame.contains(point) { + return self.buttonView + } + return super.hitTest(point, with: event) + } + func update(component: MenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component @@ -958,25 +1118,23 @@ private final class MenuComponent: Component { environment: {}, containerSize: availableSize ) - let componentFrame = CGRect(origin: CGPoint(x: 80.0, y: 80.0), size: componentSize) + let backgroundFrame = CGRect(origin: CGPoint(x: component.sourceFrame.maxX - componentSize.width, y: component.sourceFrame.minY - componentSize.height - 20.0), size: componentSize) if let view = componentView.view { if view.superview == nil { - self.addSubview(view) + self.backgroundView.contentView.addSubview(view) } - componentTransition.setFrame(view: view, frame: componentFrame) + componentTransition.setFrame(view: view, frame: CGRect(origin: .zero, size: componentSize)) } - let tintColor = GlassBackgroundView.TintColor(kind: .custom, color: UIColor(rgb: 0xf6f7f8)) - - let backgroundFrame = CGRect(origin: CGPoint(x: 80.0, y: 80.0), size: componentSize) - self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: false, tintColor: tintColor, transition: transition) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) self.backgroundView.frame = backgroundFrame - return CGSize(width: componentSize.width + 80.0 * 2.0, height: componentSize.height + 80.0 * 2.0) - } - - public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return self.backgroundView.frame.contains(point) + self.containerView.frame = CGRect(origin: .zero, size: availableSize) + self.containerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + + self.buttonView.frame = CGRect(origin: .zero, size: availableSize) + + return availableSize } } diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift index 78097beae1..ae83820ecd 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -262,10 +262,15 @@ private final class SheetPageContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + section.size.height / 2.0)) ) contentSize.height += section.size.height - contentSize.height += 16.0 + + let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 26.0, sideInset: 16.0) + contentSize.height += bottomInsets.bottom if case let .comment(isOptional, option) = component.content { - contentSize.height -= 16.0 + contentSize.height -= bottomInsets.bottom + contentSize.height += 24.0 + + let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let action = component.action let button = button.update( @@ -285,17 +290,18 @@ private final class SheetPageContent: CombinedComponent { } ), environment: {}, - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 52.0), + availableSize: CGSize(width: context.availableSize.width - bottomInsets.left - bottomInsets.right, height: 52.0), transition: context.transition ) context.add(button .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height - contentSize.height += 16.0 - if environment.inputHeight.isZero && environment.safeInsets.bottom > 0.0 { - contentSize.height += environment.safeInsets.bottom + if environment.inputHeight > 0.0 { + contentSize.height += 8.0 + } else { + contentSize.height += bottomInsets.bottom } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index fe970c1f46..b77bc4a5bc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -140,6 +140,7 @@ public final class GiftItemComponent: Component { case grid case select case buttonIcon + case tableIcon } let context: AccountContext @@ -403,6 +404,10 @@ public final class GiftItemComponent: Component { size = CGSize(width: 26.0, height: 26.0) iconSize = size cornerRadius = 0.0 + case .tableIcon: + size = CGSize(width: 18.0, height: 18.0) + iconSize = size + cornerRadius = 0.0 } var backgroundSize = size if case .grid = component.mode { @@ -504,7 +509,7 @@ public final class GiftItemComponent: Component { } } - if case .buttonIcon = component.mode { + if [.buttonIcon, .tableIcon].contains(component.mode) { backgroundColor = nil secondBackgroundColor = nil patternColor = nil @@ -846,7 +851,11 @@ public final class GiftItemComponent: Component { if let backgroundColor, let _ = secondBackgroundColor { self.backgroundLayer.backgroundColor = backgroundColor.cgColor } else { - self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor + if [.buttonIcon, .tableIcon].contains(component.mode) { + + } else { + self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor + } } let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index a75f4ba110..e91bbd2990 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -36,6 +36,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd let entities: [MessageTextEntity] let upgradeStars: Int64? let chargeStars: Int64? + let bottomInset: CGFloat init( context: AccountContext, @@ -54,7 +55,8 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd text: String, entities: [MessageTextEntity], upgradeStars: Int64?, - chargeStars: Int64? + chargeStars: Int64?, + bottomInset: CGFloat = 0.0 ) { self.context = context self.theme = theme @@ -73,6 +75,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd self.entities = entities self.upgradeStars = upgradeStars self.chargeStars = chargeStars + self.bottomInset = bottomInset } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -155,6 +158,9 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd if lhs.upgradeStars != rhs.upgradeStars { return false } + if lhs.bottomInset != rhs.bottomInset { + return false + } return true } } @@ -293,8 +299,9 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { nodes = messageNodes } + let baseContentHeight: CGFloat = 370.0 var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) - contentSize.height = 370.0 + contentSize.height = baseContentHeight + item.bottomInset insets = itemListNeighborsGroupedInsets(neighbors, params) if params.width <= 320.0 { insets.top = 0.0 @@ -326,7 +333,8 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { totalHeight += bubbleHeight } - var originY: CGFloat = floor((contentSize.height - totalHeight) / 2.0) + var originY: CGFloat = floor((baseContentHeight - totalHeight) / 2.0) + originY = contentSize.height - originY - totalHeight for node in nodes { if node.supernode == nil { strongSelf.containerNode.addSubnode(node) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index face2d1edb..f3a1f7436d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -17,7 +17,6 @@ import MultilineTextWithEntitiesComponent import BalancedTextComponent import ListSectionComponent import ListActionItemComponent -import ListMultilineTextFieldItemComponent import ListItemComponentAdaptor import BundleIconComponent import LottieComponent @@ -38,8 +37,10 @@ import UndoUI import ConfettiEffect import EdgeEffect import AnimatedTextComponent +import GlassBarButtonComponent +import MessageInputPanelComponent -final class GiftSetupScreenComponent: Component { +private final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext @@ -58,31 +59,48 @@ final class GiftSetupScreenComponent: Component { self.subject = subject self.completion = completion } - + static func ==(lhs: GiftSetupScreenComponent, rhs: GiftSetupScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.peerId != rhs.peerId { - return false - } - if lhs.subject != rhs.subject { - 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 touchesShouldCancel(in view: UIView) -> Bool { - return true + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) } } final class View: UIView, UIScrollViewDelegate { - private let topOverscrollLayer = SimpleLayer() + 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 bottomEdgeEffectView: EdgeEffectView + + private let backgroundHandleView: UIImageView + + private let closeButton = ComponentView() - private let navigationTitle = ComponentView() private let remainingCount = ComponentView() private let auctionFooter = ComponentView() private let resaleSection = ComponentView() @@ -91,25 +109,21 @@ final class GiftSetupScreenComponent: Component { private let starsSection = ComponentView() private let upgradeSection = ComponentView() private let hideSection = ComponentView() - - private let edgeEffectView: EdgeEffectView - private let buttonBackground = ComponentView() - private let buttonSeparator = SimpleLayer() - private let button = ComponentView() + + private let inputPanel = ComponentView() + private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() + + private let actionButton = ComponentView() private var ignoreScrolling: Bool = false - private var isUpdating: Bool = false private var component: GiftSetupScreenComponent? - private(set) weak var state: EmptyComponentState? - private var environment: EnvironmentType? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? - private let introPlaceholderTag = NSObject() - private let textInputState = ListMultilineTextFieldItemComponent.ExternalState() - private let textInputTag = NSObject() - private var resetText: String? - - private var currentInputMode: ListMultilineTextFieldItemComponent.InputMode = .keyboard + private var currentInputMode: MessageInputPanelComponent.InputMode = .text private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? private var inputMediaNodeDataDisposable: Disposable? @@ -119,6 +133,7 @@ final class GiftSetupScreenComponent: Component { private var inputMediaNodeBackground = SimpleLayer() private var inputMediaNodeTargetTag: AnyObject? private let inputMediaNodeDataPromise = Promise() + private var previousInputHeight: CGFloat? private var currentEmojiSuggestionView: ComponentHostView? @@ -127,11 +142,7 @@ final class GiftSetupScreenComponent: Component { private var payWithStars = false private var inProgress = false - - private var previousHadInputHeight: Bool = false - private var previousInputHeight: CGFloat? - private var recenterOnTag: NSObject? - + private var peerMap: [EnginePeer.Id: EnginePeer] = [:] private var sendPaidMessageStars: StarsAmount? @@ -140,7 +151,7 @@ final class GiftSetupScreenComponent: Component { private var giftAuctionDisposable: Disposable? private var giftAuctionTimer: SwiftSignalKit.Timer? - private var starImage: (UIImage, PresentationTheme)? + private var cachedStarImage: (UIImage, PresentationTheme)? private var updateDisposable: Disposable? @@ -156,30 +167,61 @@ final class GiftSetupScreenComponent: Component { private var cachedChevronImage: (UIImage, PresentationTheme)? override init(frame: CGRect) { + self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 38.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 38.0 + + self.backgroundHandleView = UIImageView() + + self.navigationBarContainer = SparseContainerView() + self.scrollView = ScrollView() - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.scrollsToTop = false - self.scrollView.delaysContentTouches = false + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.bottomEdgeEffectView = EdgeEffectView() + + 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.contentInsetAdjustmentBehavior = .never + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false self.scrollView.alwaysBounceVertical = true - - self.edgeEffectView = EdgeEffectView() - - super.init(frame: frame) - + self.scrollView.scrollsToTop = false self.scrollView.delegate = self - self.addSubview(self.scrollView) + self.scrollView.clipsToBounds = true - self.scrollView.layer.addSublayer(self.topOverscrollLayer) + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) - self.addSubview(self.edgeEffectView) - - self.disablesInteractiveKeyboardGestureRecognizer = true + self.scrollView.addSubview(self.scrollContentView) + + self.containerView.addSubview(self.navigationBarContainer) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { @@ -193,54 +235,93 @@ final class GiftSetupScreenComponent: Component { self.giftAuctionDisposable?.dispose() self.giftAuctionTimer?.invalidate() } - - func scrollToTop() { - self.scrollView.setContentOffset(CGPoint(), animated: true) - } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } - - private var scrolledUp = true - private func updateScrolling(transition: ComponentTransition) { - let navigationRevealOffsetY: CGFloat = 0.0 - - let navigationAlphaDistance: CGFloat = 16.0 - let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) - transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil } - var scrolledUp = false - if navigationAlpha < 0.5 { - scrolledUp = true - } else if navigationAlpha > 0.5 { - scrolledUp = false + if !self.backgroundLayer.frame.contains(point) { + return self.dimView } - if self.scrolledUp != scrolledUp { - self.scrolledUp = scrolledUp - if !self.isUpdating { - self.state?.updated() - } + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result } - if let navigationTitleView = self.navigationTitle.view { - transition.setAlpha(view: navigationTitleView, alpha: 1.0) - } - - let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) - let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 - self.buttonBackground.view?.alpha = bottomPanelAlpha - self.buttonSeparator.opacity = Float(bottomPanelAlpha) + let result = super.hitTest(point, with: event) + return result } - private var openedAuction: Bool = false + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + 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) + } + + func animateIn() { + 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) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + self.bottomEdgeEffectView.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) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + self.bottomEdgeEffectView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } @objc private func proceed() { guard let component = self.component, let environment = self.environment else { @@ -258,15 +339,14 @@ final class GiftSetupScreenComponent: Component { navigationController.pushViewController(controller) } - if self.openedAuction { + //if self.openedAuction { openAuction() - } else { - self.openedAuction = true - let controller = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: { - openAuction() - }) - environment.controller()?.push(controller) - } +// } else { +// let controller = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: { +// openAuction() +// }) +// environment.controller()?.push(controller) +// } return } @@ -315,8 +395,12 @@ final class GiftSetupScreenComponent: Component { addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") - let entities = generateChatInputTextEntities(self.textInputState.text) - let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.string, entities: entities) + var textInputText = NSAttributedString() + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(text) = inputPanelView.getSendMessageInput() { + textInputText = text + } + let entities = generateChatInputTextEntities(textInputText) + let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: textInputText.string, entities: entities) let quantity: Int32 = 1 let completion = component.completion @@ -405,7 +489,11 @@ final class GiftSetupScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let peerId = component.peerId - let entities = generateChatInputTextEntities(self.textInputState.text) + var textInputText = NSAttributedString() + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(text) = inputPanelView.getSendMessageInput() { + textInputText = text + } + let entities = generateChatInputTextEntities(textInputText) var finalPrice: Int64 var perUserLimit: Int32? @@ -415,7 +503,7 @@ final class GiftSetupScreenComponent: Component { case let .premium(product): if let option = product.starsGiftOption { finalPrice = option.amount - source = .premiumGift(peerId: peerId, option: option, text: self.textInputState.text.string, entities: entities) + source = .premiumGift(peerId: peerId, option: option, text: textInputText.string, entities: entities) } else { fatalError() } @@ -426,7 +514,7 @@ final class GiftSetupScreenComponent: Component { } perUserLimit = starGift.perUserLimit?.total giftFile = starGift.file - source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) + source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: textInputText.string, entities: entities) } let proceed = { [weak self] in @@ -612,1252 +700,7 @@ final class GiftSetupScreenComponent: Component { } @objc private func previewTap() { - func hasFirstResponder(_ view: UIView) -> Bool { - if view.isFirstResponder { - return true - } - for subview in view.subviews { - if hasFirstResponder(subview) { - return true - } - } - return false - } - - self.currentInputMode = .keyboard - if hasFirstResponder(self) { - if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - if titleView.isActive { - titleView.deactivateInput() - } else { - self.endEditing(true) - } - } - } else { - self.state?.updated(transition: .spring(duration: 0.4)) - } - } - - func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" - let isSelfGift = component.peerId == component.context.account.peerId - let isChannelGift = component.peerId.namespace == Namespaces.Peer.CloudChannel - - if self.component == nil { - if isSelfGift { - self.hideName = true - } - - if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction) { - let giftAuction = GiftAuctionContext(account: component.context.account, giftId: gift.id) - self.giftAuction = giftAuction - self.giftAuctionDisposable = (giftAuction.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let self else { - return - } - self.giftAuctionState = state - self.state?.updated() - }) - - self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in - self?.state?.updated() - }, queue: Queue.mainQueue()) - self.giftAuctionTimer?.start() - } - - var releasedBy: EnginePeer.Id? - if case let .starGift(gift, true) = component.subject, gift.upgradeStars != nil { - self.includeUpgrade = true - } - if case let .starGift(gift, _) = component.subject { - releasedBy = gift.releasedBy - } - - var peerIds: [EnginePeer.Id] = [ - component.context.account.peerId, - component.peerId - ] - if let releasedBy { - peerIds.append(releasedBy) - } - - let _ = combineLatest(queue: Queue.mainQueue(), - component.context.engine.data.get(EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in - return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - } - )), - component.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: component.peerId) - ) - ).start(next: { [weak self] peers, sendPaidMessageStars in - guard let self else { - return - } - var peersMap: [EnginePeer.Id: EnginePeer] = [:] - for (peerId, maybePeer) in peers { - if let peer = maybePeer { - peersMap[peerId] = peer - } - } - self.peerMap = peersMap - self.sendPaidMessageStars = sendPaidMessageStars - - self.state?.updated() - }) - - self.inputMediaNodeDataPromise.set( - ChatEntityKeyboardInputNode.inputData( - context: component.context, - chatPeerId: nil, - areCustomEmojiEnabled: true, - hasTrending: false, - hasSearch: true, - hasStickers: false, - hasGifs: false, - hideBackground: true, - forceHasPremium: true, - sendGif: nil - ) - ) - self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let self else { - return - } - self.inputMediaNodeData = value - }) - - self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( - sendSticker: { _, _, _, _, _, _, _, _, _ in - return false - }, - sendEmoji: { _, _, _ in - let _ = self - }, - sendGif: { _, _, _, _, _ in - return false - }, - sendBotContextResultAsGif: { _, _ , _, _, _, _ in - return false - }, - updateChoosingSticker: { _ in - }, - switchToTextInput: { [weak self] in - guard let self else { - return - } - self.currentInputMode = .keyboard - self.state?.updated(transition: .spring(duration: 0.4)) - }, - dismissTextInput: { - }, - insertText: { [weak self] text in - guard let self else { - return - } - if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - textInputView.insertText(text: text) - } - }, - backwardsDeleteText: { [weak self] in - guard let self else { - return - } - if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - if self.textInputState.isEditing { - textInputView.backwardsDeleteText() - } - } - }, - openStickerEditor: { - }, - presentController: { [weak self] c, a in - guard let self else { - return - } - self.environment?.controller()?.present(c, in: .window(.root), with: a) - }, - presentGlobalOverlayController: { [weak self] c, a in - guard let self else { - return - } - self.environment?.controller()?.presentInGlobalOverlay(c, with: a) - }, - getNavigationController: { [weak self] () -> NavigationController? in - guard let self else { - return nil - } - guard let controller = self.environment?.controller() as? GiftSetupScreen else { - return nil - } - - if let navigationController = controller.navigationController as? NavigationController { - return navigationController - } - return nil - }, - requestLayout: { [weak self] transition in - guard let self else { - return - } - if !self.isUpdating { - self.state?.updated(transition: ComponentTransition(transition)) - } - } - ) - - self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() - |> deliverOnMainQueue).start(next: { [weak self] options in - guard let self else { - return - } - self.options = options - }) - - if case let .starGift(gift, _) = component.subject { - if let _ = gift.upgradeStars { - self.previewPromise.set( - component.context.engine.payments.starGiftUpgradePreview(giftId: gift.id) - ) - } - - self.updateDisposable = component.context.engine.payments.keepStarGiftsUpdated().start() - } - } - - let environment = environment[EnvironmentType.self].value - let themeUpdated = self.environment?.theme !== environment.theme - self.environment = environment - - self.component = component - self.state = state - - if themeUpdated { - self.backgroundColor = environment.theme.list.blocksBackgroundColor - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - let navigationTitleString: String - if isSelfGift { - navigationTitleString = environment.strings.Gift_SendSelf_Title - } else if isChannelGift { - navigationTitleString = environment.strings.Gift_SendChannel_Title - } else { - var peerName = peerName - if peerName.count > 22 { - peerName = "\(peerName.prefix(22))…" - } - navigationTitleString = environment.strings.Gift_Send_TitleTo(peerName).string - } - let navigationTitleSize = self.navigationTitle.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: navigationTitleString, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) - ) - let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) - if let navigationTitleView = self.navigationTitle.view { - if navigationTitleView.superview == nil { - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - navigationBar.view.addSubview(navigationTitleView) - } - } - transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) - } - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 24.0 - - var contentHeight: CGFloat = 0.0 - - contentHeight += environment.navigationHeight - contentHeight += 26.0 - - if case let .starGift(starGift, forceUnique) = component.subject, let availability = starGift.availability, availability.resale > 0 { - if let forceUnique, !forceUnique { - } else { - let resaleSectionSize = self.resaleSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: nil, - footer: nil, - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Gift_Send_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) - ) - )), - ], alignment: .left, spacing: 2.0)), - accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator), - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemSecondaryTextColor - )), - maximumNumberOfLines: 0 - ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), - action: { [weak self] _ in - guard let self, let component = self.component, let controller = environment.controller() else { - return - } - let storeController = component.context.sharedContext.makeGiftStoreController( - context: component.context, - peerId: component.peerId, - gift: starGift - ) - controller.push(storeController) - } - ))) - ] - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize) - if let resaleSectionView = self.resaleSection.view { - if resaleSectionView.superview == nil { - self.scrollView.addSubview(resaleSectionView) - } - transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame) - } - contentHeight += resaleSectionSize.height - contentHeight += sectionSpacing - } - } - - let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) - - var introSectionItems: [AnyComponentWithIdentity] = [] - introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 370.0, tag: self.introPlaceholderTag)))) - - if self.sendPaidMessageStars == nil { - introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent( - externalState: self.textInputState, - context: component.context, - theme: environment.theme, - strings: environment.strings, - initialText: "", - resetText: self.resetText.flatMap { - return ListMultilineTextFieldItemComponent.ResetText(value: $0) - }, - placeholder: environment.strings.Gift_Send_Customize_MessagePlaceholder, - autocapitalizationType: .sentences, - autocorrectionType: .yes, - returnKeyType: .done, - characterLimit: Int(giftConfiguration.maxCaptionLength), - displayCharacterLimit: true, - emptyLineHandling: .notAllowed, - formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]), - updated: { _ in - }, - returnKeyAction: { [weak self] in - guard let self else { - return - } - if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - titleView.endEditing(true) - } - }, - textUpdateTransition: .spring(duration: 0.4), - inputMode: self.currentInputMode, - toggleInputMode: { [weak self] in - guard let self else { - return - } - switch self.currentInputMode { - case .keyboard: - self.currentInputMode = .emoji - case .emoji: - self.currentInputMode = .keyboard - } - self.state?.updated(transition: .spring(duration: 0.4)) - }, - tag: self.textInputTag - )))) - self.resetText = nil - } - - let footerAttributes = MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), - link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), - linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - } - ) - - let introFooter: AnyComponent? - switch component.subject { - case .premium: - introFooter = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_Customize_Info(peerName).string, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )) - case .starGift: - introFooter = nil - } - - let introSectionSize = self.introSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: nil, - footer: introFooter, - items: introSectionItems - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize) - if let introSectionView = self.introSection.view { - if introSectionView.superview == nil { - self.scrollView.addSubview(introSectionView) - self.introSection.parentState = state - } - transition.setFrame(view: introSectionView, frame: introSectionFrame) - } - contentHeight += introSectionSize.height - contentHeight += sectionSpacing - - var inputHeight: CGFloat = 0.0 - inputHeight += self.updateInputMediaNode( - component: component, - availableSize: availableSize, - bottomInset: environment.safeInsets.bottom, - inputHeight: 0.0, - effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), - metrics: environment.metrics, - deviceMetrics: environment.deviceMetrics, - transition: transition - ) - if self.inputMediaNode == nil { - if environment.inputHeight.isZero && self.textInputState.isEditing, let previousInputHeight = self.previousInputHeight { - inputHeight = previousInputHeight - } else { - inputHeight = environment.inputHeight - } - } - - let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) - if let accountPeer = self.peerMap[component.context.account.peerId] { - var upgradeStars: Int64? - let subject: ChatGiftPreviewItem.Subject - var releasedBy: EnginePeer.Id? - switch component.subject { - case let .premium(product): - if self.payWithStars, let starsPrice = product.starsPrice { - subject = .premium(months: product.months, amount: starsPrice, currency: "XTR") - } else { - let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) - subject = .premium(months: product.months, amount: amount, currency: currency) - } - case let .starGift(gift, _): - subject = .starGift(gift: gift) - upgradeStars = gift.upgradeStars - releasedBy = gift.releasedBy - } - - var peers: [EnginePeer] = [accountPeer] - if let peer = self.peerMap[component.peerId] { - peers.append(peer) - } - if let releasedBy, let peer = self.peerMap[releasedBy] { - peers.append(peer) - } - - let introContentSize = self.introContent.update( - transition: transition, - component: AnyComponent( - ListItemComponentAdaptor( - itemGenerator: ChatGiftPreviewItem( - context: component.context, - theme: environment.theme, - componentTheme: environment.theme, - strings: environment.strings, - sectionId: 0, - fontSize: presentationData.chatFontSize, - chatBubbleCorners: presentationData.chatBubbleCorners, - wallpaper: presentationData.chatWallpaper, - dateTimeFormat: environment.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - peers: peers, - subject: subject, - chatPeerId: component.peerId, - text: self.textInputState.text.string, - entities: generateChatInputTextEntities(self.textInputState.text), - upgradeStars: self.includeUpgrade ? upgradeStars : nil, - chargeStars: self.textInputState.text.string.isEmpty ? nil : 250 - ), - params: listItemParams - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - if let introContentView = self.introContent.view { - if introContentView.superview == nil { - if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) { - placeholderView.addSubview(introContentView) - - placeholderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap))) - } - } - transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) - } - } - - switch component.subject { - case let .premium(product): - let balance = component.context.starsContext?.currentState?.balance.value ?? 0 - if let starsPrice = product.starsPrice, balance >= starsPrice { - let balanceString = presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator) - - let starsFooterRawString = environment.strings.Gift_Send_PayWithStars_Info("# \(balanceString)").string - let starsFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(starsFooterRawString, attributes: footerAttributes)) - - if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { - self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) - } - if let range = starsFooterText.string.range(of: "#") { - starsFooterText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: starsFooterText.string)) - } - if let range = starsFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { - starsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsFooterText.string)) - } - - let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator) - let starsAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_PayWithStars("#\(priceString)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) - let range = (starsAttributedText.string as NSString).range(of: "#") - if range.location != NSNotFound { - starsAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) - starsAttributedText.addAttribute(.baselineOffset, value: 1.0, range: range) - } - - let starsSectionSize = self.starsSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: nil, - footer: AnyComponent(MultilineTextWithEntitiesComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - placeholderColor: .clear, - text: .plain(starsFooterText), - maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { [weak self] _, _ in - guard let self, let component = self.component, let controller = self.environment?.controller(), let starsContext = component.context.starsContext else { - return - } - let _ = (self.optionsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { options in - let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { stars in - starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) - }) - controller.push(purchaseController) - }) - } - )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( - MultilineTextWithEntitiesComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - placeholderColor: environment.theme.list.mediaPlaceholderColor, - text: .plain(starsAttributedText) - ) - )), - ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.payWithStars, action: { [weak self] _ in - guard let self else { - return - } - self.payWithStars = !self.payWithStars - self.state?.updated(transition: .spring(duration: 0.4)) - })), - action: nil - ))) - ] - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let starsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: starsSectionSize) - if let starsSectionView = self.starsSection.view { - if starsSectionView.superview == nil { - self.scrollView.addSubview(starsSectionView) - } - transition.setFrame(view: starsSectionView, frame: starsSectionFrame) - } - contentHeight += starsSectionSize.height - contentHeight += sectionSpacing - } - case let .starGift(gift, forceUnique): - if let upgradeStars = gift.upgradeStars, component.peerId != component.context.account.peerId { - let upgradeFooterRawString: String - if isChannelGift { - upgradeFooterRawString = environment.strings.Gift_SendChannel_Upgrade_Info(peerName).string - } else { - if forceUnique == true { - upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_ForcedInfo(peerName).string - } else { - upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_Info(peerName).string - } - } - let parsedString = parseMarkdownIntoAttributedString(upgradeFooterRawString, attributes: footerAttributes) - - let upgradeFooterText = NSMutableAttributedString(attributedString: parsedString) - - if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { - self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) - } - if let range = upgradeFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { - upgradeFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: upgradeFooterText.string)) - } - - let upgradeAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_Upgrade("#\(upgradeStars)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) - let range = (upgradeAttributedText.string as NSString).range(of: "#") - if range.location != NSNotFound { - upgradeAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) - upgradeAttributedText.addAttribute(.baselineOffset, value: 1.0, range: range) - } - - let upgradeSectionSize = self.upgradeSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: nil, - footer: AnyComponent(MultilineTextComponent( - text: .plain(upgradeFooterText), - maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { [weak self] _, _ in - guard let self else { - return - } - let _ = (self.previewPromise.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] upgradePreview in - guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else { - return - } - let previewController = GiftViewScreen( - context: component.context, - subject: .upgradePreview(upgradePreview.attributes, peerName) - ) - controller.push(previewController) - }) - } - )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( - MultilineTextWithEntitiesComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - placeholderColor: environment.theme.list.mediaPlaceholderColor, - text: .plain(upgradeAttributedText) - ) - )), - ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.includeUpgrade, isEnabled: forceUnique != true, action: { [weak self] _ in - guard let self, forceUnique != true else { - return - } - self.includeUpgrade = !self.includeUpgrade - self.state?.updated(transition: .spring(duration: 0.4)) - })), - action: nil - ))) - ] - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let upgradeSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: upgradeSectionSize) - if let upgradeSectionView = self.upgradeSection.view { - if upgradeSectionView.superview == nil { - self.scrollView.addSubview(upgradeSectionView) - } - transition.setFrame(view: upgradeSectionView, frame: upgradeSectionFrame) - } - contentHeight += upgradeSectionSize.height - contentHeight += sectionSpacing - } - - let hideSectionFooterString: String - if isSelfGift { - hideSectionFooterString = environment.strings.Gift_SendSelf_HideMyName_Info - } else if isChannelGift { - hideSectionFooterString = environment.strings.Gift_SendChannel_HideMyName_Info - } else { - hideSectionFooterString = environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string - } - let hideSectionSize = self.hideSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: nil, - footer: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: hideSectionFooterString, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: isSelfGift ? environment.strings.Gift_SendSelf_HideMyName : environment.strings.Gift_Send_HideMyName, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in - guard let self else { - return - } - self.hideName = !self.hideName - self.state?.updated(transition: .spring(duration: 0.4)) - })), - action: nil - ))) - ] - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) - if let hideSectionView = self.hideSection.view { - if hideSectionView.superview == nil { - self.scrollView.addSubview(hideSectionView) - } - transition.setFrame(view: hideSectionView, frame: hideSectionFrame) - } - contentHeight += hideSectionSize.height - } - contentHeight += sectionSpacing - - if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability { - contentHeight -= 77.0 - contentHeight += 16.0 - - let remains: Int32 = availability.remains - let total: Int32 = availability.total - let position = CGFloat(remains) / CGFloat(total) - let sold = total - remains - let remainingCountSize = self.remainingCount.update( - transition: transition, - component: AnyComponent(RemainingCountComponent( - inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), - activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], - inactiveTitle: environment.strings.Gift_Send_Remains(remains), - inactiveValue: "", - inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, - activeTitle: "", - activeValue: environment.strings.Gift_Send_Sold(sold),//totalString, - activeTitleColor: .white, - badgeText: "", - badgePosition: position, - badgeGraphPosition: position, - invertProgress: true, - leftString: "", - groupingSeparator: environment.dateTimeFormat.groupingSeparator - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: remainingCountSize) - if let remainingCountView = self.remainingCount.view { - if remainingCountView.superview == nil { - self.scrollView.addSubview(remainingCountView) - } - transition.setFrame(view: remainingCountView, frame: remainingCountFrame) - } - contentHeight += remainingCountSize.height - contentHeight += 7.0 - - if starGift.flags.contains(.isAuction) { - let parsedString = parseMarkdownIntoAttributedString("50 gifts are dropped at varying intervals to the top 50 bidders by bid amount. [Learn more >]()", attributes: footerAttributes) - let auctionFooterText = NSMutableAttributedString(attributedString: parsedString) - - if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { - self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) - } - if let range = auctionFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { - auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string)) - } - - let auctionFooterSize = self.auctionFooter.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .plain(auctionFooterText), - maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { [weak self] _, _ in - guard let self, let component = self.component, case let .starGift(gift, _) = component.subject, let controller = self.environment?.controller() else { - return - } - let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: nil) - controller.push(infoController) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0) - ) - let auctionFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: auctionFooterSize) - if let auctionFooterView = self.auctionFooter.view { - if auctionFooterView.superview == nil { - self.scrollView.addSubview(auctionFooterView) - } - transition.setFrame(view: auctionFooterView, frame: auctionFooterFrame) - } - contentHeight += remainingCountSize.height - } - - contentHeight += sectionSpacing - } - - - let buttonHeight: CGFloat = 50.0 - let bottomPanelPadding: CGFloat = 12.0 - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - - let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom) - contentHeight += max(bottomPanelHeight, combinedBottomInset) - - if self.starImage == nil || self.starImage?.1 !== environment.theme { - self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) - } - - let bottomPanelSize = self.buttonBackground.update( - transition: transition, - component: AnyComponent(BlurredBackgroundComponent( - color: environment.theme.rootController.tabBar.backgroundColor - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight) - ) - self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor - - if let view = self.buttonBackground.view { -// if view.superview == nil { -// self.addSubview(view) -// self.layer.addSublayer(self.buttonSeparator) -// } - view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize) - self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel)) - } - - var buttonIsEnabled = true - let buttonString: String - switch component.subject { - case let .premium(product): - if self.payWithStars, let starsPrice = product.starsPrice { - let amountString = presentationStringsFormattedNumber(Int32(starsPrice), presentationData.dateTimeFormat.groupingSeparator) - buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" - } else { - let amountString = product.price - buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" - } - case let .starGift(starGift, _): - var finalPrice: Int64 = starGift.price - if self.includeUpgrade, let upgradePrice = starGift.upgradeStars { - finalPrice += upgradePrice - } - let amountString = presentationStringsFormattedNumber(Int32(finalPrice), presentationData.dateTimeFormat.groupingSeparator) - let buttonTitle = isSelfGift ? environment.strings.Gift_Send_Buy : environment.strings.Gift_Send_Send - buttonString = "\(buttonTitle) # \(amountString)" - if let availability = starGift.availability, availability.remains == 0 { - buttonIsEnabled = false - } - } - - var buttonTitleItems: [AnyComponentWithIdentity] = [] - - let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 { - buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string)) - buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) - } - - var buttonIsLoading = false - if let _ = self.giftAuction { - //TODO:localize - let buttonAttributedString = NSMutableAttributedString(string: "Place a Bid", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - buttonTitleItems.append(AnyComponentWithIdentity(id: "bid", component: AnyComponent( - MultilineTextComponent(text: .plain(buttonAttributedString)) - ))) - if let giftAuctionState = self.giftAuctionState { - switch giftAuctionState.auctionState { - case let .ongoing(_, _, _, _, _, nextDropDate, _, _): - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - let dropTimeout = nextDropDate - currentTime - - let minutes = Int(dropTimeout / 60) - let seconds = Int(dropTimeout % 60) - - let rawString = environment.strings.Gift_Setup_NextDropIn - var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] - var startIndex = rawString.startIndex - while true { - if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) { - if range.lowerBound != startIndex { - buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< range.lowerBound])))) - } - - startIndex = range.upperBound - if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) { - let controlString = rawString[range.upperBound ..< endRange.lowerBound] - if controlString == "m" { - buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(minutes, minDigits: 2))) - } else if controlString == "s" { - buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(seconds, minDigits: 2))) - } - - startIndex = endRange.upperBound - } - } else { - break - } - } - if startIndex != rawString.endIndex { - buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< rawString.endIndex])))) - } - - buttonTitleItems.append(AnyComponentWithIdentity(id: "timer", component: AnyComponent(AnimatedTextComponent( - font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers), - color: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), - items: buttonAnimatedTitleItems, - noDelay: true - )))) - case .finished: - buttonIsEnabled = false - } - } else { - buttonIsLoading = true - } - } else { - buttonTitleItems.append(AnyComponentWithIdentity(id: buttonString, component: AnyComponent( - MultilineTextComponent(text: .plain(buttonAttributedString)) - ))) - } - - let buttonSideInset = environment.safeInsets.left + 36.0 - let buttonSize = self.button.update( - transition: .spring(duration: 0.2), - 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.9), - isShimmering: true - ), - content: AnyComponentWithIdentity( - id: AnyHashable("title"), - component: AnyComponent(VStack(buttonTitleItems, spacing: 1.0)) - ), - isEnabled: buttonIsEnabled, - displaysProgress: buttonIsLoading || self.inProgress, - action: { [weak self] in - self?.proceed() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: buttonHeight) - ) - if let buttonView = self.button.view { - if buttonView.superview == nil { - self.addSubview(buttonView) - } - buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) - } - - let controller = environment.controller() - if inputHeight > 10.0 { - if self.inProgress { - let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: environment.theme.rootController.navigationBar.accentTextColor)) - controller?.navigationItem.rightBarButtonItem = item - } else { - let rightBarButtonItem = UIBarButtonItem(title: environment.strings.Gift_Send_SendShort, style: .done, target: self, action: #selector(self.proceed)) - rightBarButtonItem.isEnabled = buttonIsEnabled - controller?.navigationItem.setRightBarButton(rightBarButtonItem, animated: controller?.navigationItem.rightBarButtonItem == nil) - } - } else { - controller?.navigationItem.setRightBarButton(nil, animated: true) - } - - if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { - emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) - |> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in - guard let self, self.textInputState.currentEmojiSuggestion === emojiSuggestion else { - return - } - - emojiSuggestion?.value = result - self.state?.updated() - }) - } - - var hasTrackingView = self.textInputState.hasTrackingView - if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { - hasTrackingView = false - } - if !self.textInputState.isEditing { - hasTrackingView = false - } - - if !hasTrackingView { - if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion { - self.textInputState.currentEmojiSuggestion = nil - currentEmojiSuggestion.disposable?.dispose() - } - - if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { - self.currentEmojiSuggestionView = nil - - currentEmojiSuggestionView.alpha = 0.0 - currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in - currentEmojiSuggestionView?.removeFromSuperview() - }) - } - } - - if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] { - let currentEmojiSuggestionView: ComponentHostView - if let current = self.currentEmojiSuggestionView { - currentEmojiSuggestionView = current - } else { - currentEmojiSuggestionView = ComponentHostView() - self.currentEmojiSuggestionView = currentEmojiSuggestionView - self.addSubview(currentEmojiSuggestionView) - - currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - - let globalPosition: CGPoint - if let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView { - globalPosition = textView.convert(emojiSuggestion.localPosition, to: self) - } else { - globalPosition = .zero - } - - let sideInset: CGFloat = 7.0 - - let viewSize = currentEmojiSuggestionView.update( - transition: .immediate, - component: AnyComponent(EmojiSuggestionsComponent( - context: component.context, - userLocation: .other, - theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor), - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - files: value, - action: { [weak self] file in - guard let self, let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView, let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion else { - return - } - - AudioServicesPlaySystemSound(0x450) - - let inputState = textView.getInputState() - let inputText = NSMutableAttributedString(attributedString: inputState.inputText) - - var text: String? - var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? - loop: for attribute in file.attributes { - switch attribute { - case let .CustomEmoji(_, _, displayText, _): - text = displayText - emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) - break loop - default: - break - } - } - - if let emojiAttribute = emojiAttribute, let text = text { - let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) - - let range = currentEmojiSuggestion.position.range - let previousText = inputText.attributedSubstring(from: range) - inputText.replaceCharacters(in: range, with: replacementText) - - var replacedUpperBound = range.lowerBound - while true { - if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { - let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) - if replaceRange.location < 0 { - break - } - let adjacentString = inputText.attributedSubstring(from: replaceRange) - if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { - break - } - inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) - replacedUpperBound = replaceRange.lowerBound - } else { - break - } - } - - let selectionPosition = range.lowerBound + (replacementText.string as NSString).length - textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) - ) - - let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) - currentEmojiSuggestionView.frame = viewFrame - if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { - componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0)) - } - } - - let previousBounds = self.scrollView.bounds - - self.recenterOnTag = nil - if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { - if let textView = self.introSection.findTaggedView(tag: self.textInputTag) { - if targetView.isDescendant(of: textView) { - self.recenterOnTag = self.textInputTag - } - } - } - if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode { - if self.textInputState.isEditing { - self.recenterOnTag = self.textInputTag - } - } - self.previousHadInputHeight = inputHeight > 0.0 - self.previousInputHeight = inputHeight - - self.ignoreScrolling = true - let contentSize = CGSize(width: availableSize.width, height: contentHeight) - if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { - self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) - } - if self.scrollView.contentSize != contentSize { - self.scrollView.contentSize = contentSize - } - let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) - if self.scrollView.verticalScrollIndicatorInsets != scrollInsets { - self.scrollView.verticalScrollIndicatorInsets = scrollInsets - } - - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - - if let recenterOnTag = self.recenterOnTag { - self.recenterOnTag = nil - - if let targetView = self.introSection.findTaggedView(tag: recenterOnTag) { - let caretRect = targetView.convert(targetView.bounds, to: self.scrollView) - var scrollViewBounds = self.scrollView.bounds - let minButtonDistance: CGFloat = 16.0 - if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance { - scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY) - if scrollViewBounds.origin.y < 0.0 { - scrollViewBounds.origin.y = 0.0 - } - } - if self.scrollView.bounds != scrollViewBounds { - transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) - } - } - } - - let edgeEffectHeight: CGFloat = bottomPanelHeight - let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) - transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition) - - self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) - self.ignoreScrolling = false - - self.updateScrolling(transition: transition) - - return availableSize + self.deactivateInput() } private func updateInputMediaNode( @@ -1987,7 +830,7 @@ final class GiftSetupScreenComponent: Component { guard let self else { return } - if self.currentInputMode == .keyboard { + if self.currentInputMode == .text { self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in guard let self else { return @@ -2003,21 +846,1203 @@ final class GiftSetupScreenComponent: Component { }) } } - + return height } + + private func activateInput() { + self.currentInputMode = .text + if !hasFirstResponder(self) { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + view.activateInput() + } + } else { + self.state?.updated(transition: .immediate) + } + } + + private var nextTransitionUserData: Any? + @objc private func deactivateInput() { + guard let _ = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + self.currentInputMode = .text + if hasFirstResponder(self) { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + self.nextTransitionUserData = TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)) + if view.isActive { + view.deactivateInput(force: true) + } else { + self.endEditing(true) + } + } + } else { + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)))) + } + } + + func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = environment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, 428.0) - environment.safeInsets.left * 2.0 + } + let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 24.0 + + let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" + let isSelfGift = component.peerId == component.context.account.peerId + let isChannelGift = component.peerId.namespace == Namespaces.Peer.CloudChannel + + if self.component == nil { + if isSelfGift { + self.hideName = true + } + + if case let .starGift(gift, _) = component.subject, gift.flags.contains(.isAuction) { + let giftAuction = GiftAuctionContext(account: component.context.account, giftId: gift.id) + self.giftAuction = giftAuction + self.giftAuctionDisposable = (giftAuction.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.giftAuctionState = state + self.state?.updated() + }) + + self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.state?.updated() + }, queue: Queue.mainQueue()) + self.giftAuctionTimer?.start() + } + + var releasedBy: EnginePeer.Id? + if case let .starGift(gift, true) = component.subject, gift.upgradeStars != nil { + self.includeUpgrade = true + } + if case let .starGift(gift, _) = component.subject { + releasedBy = gift.releasedBy + } + + var peerIds: [EnginePeer.Id] = [ + component.context.account.peerId, + component.peerId + ] + if let releasedBy { + peerIds.append(releasedBy) + } + + let _ = combineLatest(queue: Queue.mainQueue(), + component.context.engine.data.get(EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + )), + component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars(id: component.peerId) + ) + ).start(next: { [weak self] peers, sendPaidMessageStars in + guard let self else { + return + } + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for (peerId, maybePeer) in peers { + if let peer = maybePeer { + peersMap[peerId] = peer + } + } + self.peerMap = peersMap + self.sendPaidMessageStars = sendPaidMessageStars + + self.state?.updated() + }) + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: component.context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: false, + hasGifs: false, + hideBackground: true, + forceHasPremium: true, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + let _ = self + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _ , _, _, _, _ in + return false + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + guard let self else { + return + } + self.currentInputMode = .text + self.state?.updated(transition: .spring(duration: 0.4)) + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + guard let self else { + return + } + self.inputPanelExternalState.insertText(text) + }, + backwardsDeleteText: { [weak self] in + guard let self else { + return + } + self.inputPanelExternalState.deleteBackward() + }, + openStickerEditor: { + }, + presentController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.present(c, in: .window(.root), with: a) + }, + presentGlobalOverlayController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.presentInGlobalOverlay(c, with: a) + }, + getNavigationController: { [weak self] () -> NavigationController? in + guard let self else { + return nil + } + guard let controller = self.environment?.controller() as? GiftSetupScreen else { + return nil + } + + if let navigationController = controller.navigationController as? NavigationController { + return navigationController + } + return nil + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: ComponentTransition(transition)) + } + } + ) + + self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + + if case let .starGift(gift, _) = component.subject { + if let _ = gift.upgradeStars { + self.previewPromise.set( + component.context.engine.payments.starGiftUpgradePreview(giftId: gift.id) + ) + } + + self.updateDisposable = component.context.engine.payments.keepStarGiftsUpdated().start() + } + } + + self.component = component + self.state = state + self.environment = environment + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let sectionSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + + if self.backgroundHandleView.image == nil { + self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.backgroundHandleView.tintColor = environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.2) + 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) + } + transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) + + let closeButtonSize = self.closeButton.update( + transition: .immediate, + 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.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + if case let .starGift(starGift, forceUnique) = component.subject, let availability = starGift.availability, availability.resale > 0 { + if let forceUnique, !forceUnique { + } else { + let resaleSectionSize = self.resaleSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Gift_Send_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) + ) + )), + ], alignment: .left, spacing: 2.0)), + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator), + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0 + ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), + action: { [weak self] _ in + guard let self, let component = self.component, let controller = environment.controller() else { + return + } + let storeController = component.context.sharedContext.makeGiftStoreController( + context: component.context, + peerId: component.peerId, + gift: starGift + ) + controller.push(storeController) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize) + if let resaleSectionView = self.resaleSection.view { + if resaleSectionView.superview == nil { + self.scrollContentView.addSubview(resaleSectionView) + } + transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame) + } + contentHeight += resaleSectionSize.height + contentHeight += sectionSpacing + } + } + + let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + + let footerAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + +// let introFooter: AnyComponent? +// switch component.subject { +// case .premium: +// introFooter = AnyComponent(MultilineTextComponent( +// text: .plain(NSAttributedString( +// string: environment.strings.Gift_Send_Customize_Info(peerName).string, +// font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), +// textColor: environment.theme.list.freeTextColor +// )), +// maximumNumberOfLines: 0 +// )) +// case .starGift: +// introFooter = nil +// } +// +// let introSectionSize = self.introSection.update( +// transition: transition, +// component: AnyComponent(ListSectionComponent( +// theme: environment.theme, +// style: .glass, +// header: nil, +// footer: introFooter, +// items: introSectionItems +// )), +// environment: {}, +// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) +// ) +// let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize) +// if let introSectionView = self.introSection.view { +// if introSectionView.superview == nil { +// self.scrollContentView.addSubview(introSectionView) +// self.introSection.parentState = state +// } +// transition.setFrame(view: introSectionView, frame: introSectionFrame) +// } +// contentHeight += introSectionSize.height +// contentHeight += sectionSpacing + + var inputHeight: CGFloat = 0.0 + inputHeight += self.updateInputMediaNode( + component: component, + availableSize: availableSize, + bottomInset: environment.safeInsets.bottom, + inputHeight: 0.0, + effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + transition: transition + ) + if self.inputMediaNode == nil { + if environment.inputHeight.isZero && self.inputPanelExternalState.isEditing, let previousInputHeight = self.previousInputHeight { + inputHeight = previousInputHeight + } else { + inputHeight = environment.inputHeight + } + } + self.previousInputHeight = inputHeight + + let listItemParams = ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + var introContentSize = CGSize() + if let accountPeer = self.peerMap[component.context.account.peerId] { + var inputPanelSize = CGSize() + let inputPanelInset: CGFloat = 16.0 + if self.sendPaidMessageStars == nil { + let nextInputMode: MessageInputPanelComponent.InputMode + switch self.currentInputMode { + case .text: + nextInputMode = .emoji + case .emoji: + nextInputMode = .text + default: + nextInputMode = .emoji + } + + self.inputPanel.parentState = state + inputPanelSize = self.inputPanel.update( + transition: transition, + component: AnyComponent(MessageInputPanelComponent( + externalState: self.inputPanelExternalState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .gift, + placeholder: .plain(environment.strings.Gift_Send_Customize_MessagePlaceholder), + sendPaidMessageStars: nil, + maxLength: Int(giftConfiguration.maxCaptionLength), + queryTypes: [], + alwaysDarkWhenHasText: false, + useGrayBackground: false, + resetInputContents: nil, + nextInputMode: { _ in return nextInputMode }, + areVoiceMessagesAvailable: false, + presentController: { c in + }, + presentInGlobalOverlay: { c in + }, + sendMessageAction: { [weak self] _ in + guard let self else { + return + } + self.deactivateInput() + }, + sendMessageOptionsAction: nil, + sendStickerAction: { _ in }, + setMediaRecordingActive: nil, + lockMediaRecording: { + }, + stopAndPreviewMediaRecording: { + }, + discardMediaRecordingPreview: nil, + attachmentAction: nil, + myReaction: nil, + likeAction: nil, + likeOptionsAction: nil, + inputModeAction: { [weak self] in + if let self { + switch self.currentInputMode { + case .text: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .text + default: + self.currentInputMode = .emoji + } + if self.currentInputMode == .text { + self.activateInput() + } else { + self.state?.updated(transition: .immediate) + } + } + }, + timeoutAction: nil, + forwardAction: nil, + paidMessageAction: nil, + moreAction: nil, + presentCaptionPositionTooltip: nil, + presentVoiceMessagesUnavailableTooltip: nil, + presentTextLengthLimitTooltip: { + }, + presentTextFormattingTooltip: { + }, + paste: { _ in + }, + audioRecorder: nil, + videoRecordingStatus: nil, + isRecordingLocked: false, + hasRecordedVideo: false, + recordedAudioPreview: nil, + hasRecordedVideoPreview: false, + wasRecordingDismissed: false, + timeoutValue: nil, + timeoutSelected: false, + displayGradient: false, + bottomInset: 0.0, + isFormattingLocked: false, + hideKeyboard: self.currentInputMode == .emoji, + customInputView: nil, + forceIsEditing: self.currentInputMode == .emoji, + disabledPlaceholder: nil, + header: nil, + isChannel: false, + storyItem: nil, + chatLocation: nil + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - inputPanelInset * 2.0, height: 160.0) + ) + } + + var upgradeStars: Int64? + let subject: ChatGiftPreviewItem.Subject + var releasedBy: EnginePeer.Id? + switch component.subject { + case let .premium(product): + if self.payWithStars, let starsPrice = product.starsPrice { + subject = .premium(months: product.months, amount: starsPrice, currency: "XTR") + } else { + let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) + subject = .premium(months: product.months, amount: amount, currency: currency) + } + case let .starGift(gift, _): + subject = .starGift(gift: gift) + upgradeStars = gift.upgradeStars + releasedBy = gift.releasedBy + } + + var peers: [EnginePeer] = [accountPeer] + if let peer = self.peerMap[component.peerId] { + peers.append(peer) + } + if let releasedBy, let peer = self.peerMap[releasedBy] { + peers.append(peer) + } + + var textInputText = NSAttributedString() + if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(text) = inputPanelView.getSendMessageInput(applyAutocorrection: false) { + textInputText = text + } + introContentSize = self.introContent.update( + transition: transition, + component: AnyComponent( + ListItemComponentAdaptor( + itemGenerator: ChatGiftPreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: presentationData.chatWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + peers: peers, + subject: subject, + chatPeerId: component.peerId, + text: textInputText.string, + entities: generateChatInputTextEntities(textInputText), + upgradeStars: self.includeUpgrade ? upgradeStars : nil, + chargeStars: nil, + bottomInset: max(0.0, inputPanelSize.height - 26.0) + ), + params: listItemParams + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 10000.0) + ) + if let introContentView = self.introContent.view { + if introContentView.superview == nil { + introContentView.clipsToBounds = true + introContentView.layer.cornerRadius = 38.0 + introContentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + self.scrollContentView.addSubview(introContentView) + introContentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap))) + } + transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) + } + + let inputPanelFrame = CGRect(origin: CGPoint(x: inputPanelInset, y: contentHeight + introContentSize.height - inputPanelInset - inputPanelSize.height + 6.0), size: inputPanelSize) + if let inputPanelView = self.inputPanel.view { + if inputPanelView.superview == nil { + self.scrollContentView.addSubview(inputPanelView) + } + transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + } + } + contentHeight += introContentSize.height + contentHeight += sectionSpacing + + switch component.subject { + case let .premium(product): + let balance = component.context.starsContext?.currentState?.balance.value ?? 0 + if let starsPrice = product.starsPrice, balance >= starsPrice { + let balanceString = presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator) + + let starsFooterRawString = environment.strings.Gift_Send_PayWithStars_Info("# \(balanceString)").string + let starsFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(starsFooterRawString, attributes: footerAttributes)) + + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + if let range = starsFooterText.string.range(of: "#") { + starsFooterText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: starsFooterText.string)) + } + if let range = starsFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + starsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsFooterText.string)) + } + + let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator) + let starsAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_PayWithStars("#\(priceString)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + let range = (starsAttributedText.string as NSString).range(of: "#") + if range.location != NSNotFound { + starsAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + starsAttributedText.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + let starsSectionSize = self.starsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .clear, + text: .plain(starsFooterText), + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component, let controller = self.environment?.controller(), let starsContext = component.context.starsContext else { + return + } + let _ = (self.optionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { options in + let purchaseController = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options ?? [], purpose: .generic, targetPeerId: nil, customTheme: nil, completion: { stars in + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + }) + controller.push(purchaseController) + }) + } + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + text: .plain(starsAttributedText) + ) + )), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.payWithStars, action: { [weak self] _ in + guard let self else { + return + } + self.payWithStars = !self.payWithStars + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let starsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: starsSectionSize) + if let starsSectionView = self.starsSection.view { + if starsSectionView.superview == nil { + self.scrollContentView.addSubview(starsSectionView) + } + transition.setFrame(view: starsSectionView, frame: starsSectionFrame) + } + contentHeight += starsSectionSize.height + contentHeight += sectionSpacing + } + case let .starGift(gift, forceUnique): + if let upgradeStars = gift.upgradeStars, component.peerId != component.context.account.peerId { + let upgradeFooterRawString: String + if isChannelGift { + upgradeFooterRawString = environment.strings.Gift_SendChannel_Upgrade_Info(peerName).string + } else { + if forceUnique == true { + upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_ForcedInfo(peerName).string + } else { + upgradeFooterRawString = environment.strings.Gift_Send_Upgrade_Info(peerName).string + } + } + let parsedString = parseMarkdownIntoAttributedString(upgradeFooterRawString, attributes: footerAttributes) + + let upgradeFooterText = NSMutableAttributedString(attributedString: parsedString) + + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + if let range = upgradeFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + upgradeFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: upgradeFooterText.string)) + } + + let upgradeAttributedText = NSMutableAttributedString(string: environment.strings.Gift_Send_Upgrade("#\(upgradeStars)").string, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + let range = (upgradeAttributedText.string as NSString).range(of: "#") + if range.location != NSNotFound { + upgradeAttributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + upgradeAttributedText.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + let upgradeSectionSize = self.upgradeSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(upgradeFooterText), + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self else { + return + } + let _ = (self.previewPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] upgradePreview in + guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else { + return + } + let previewController = GiftViewScreen( + context: component.context, + subject: .upgradePreview(upgradePreview.attributes, peerName) + ) + controller.push(previewController) + }) + } + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + text: .plain(upgradeAttributedText) + ) + )), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.includeUpgrade, isEnabled: forceUnique != true, action: { [weak self] _ in + guard let self, forceUnique != true else { + return + } + self.includeUpgrade = !self.includeUpgrade + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let upgradeSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: upgradeSectionSize) + if let upgradeSectionView = self.upgradeSection.view { + if upgradeSectionView.superview == nil { + self.scrollContentView.addSubview(upgradeSectionView) + } + transition.setFrame(view: upgradeSectionView, frame: upgradeSectionFrame) + } + contentHeight += upgradeSectionSize.height + contentHeight += sectionSpacing + } + + let hideSectionFooterString: String + if isSelfGift { + hideSectionFooterString = environment.strings.Gift_SendSelf_HideMyName_Info + } else if isChannelGift { + hideSectionFooterString = environment.strings.Gift_SendChannel_HideMyName_Info + } else { + hideSectionFooterString = environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string + } + let hideSectionSize = self.hideSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: hideSectionFooterString, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: isSelfGift ? environment.strings.Gift_SendSelf_HideMyName : environment.strings.Gift_Send_HideMyName, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in + guard let self else { + return + } + self.hideName = !self.hideName + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) + if let hideSectionView = self.hideSection.view { + if hideSectionView.superview == nil { + self.scrollContentView.addSubview(hideSectionView) + } + transition.setFrame(view: hideSectionView, frame: hideSectionFrame) + } + contentHeight += hideSectionSize.height + } + contentHeight += sectionSpacing + + if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability { + contentHeight -= 77.0 + contentHeight += 16.0 + + let remains: Int32 = availability.remains + let total: Int32 = availability.total + let position = CGFloat(remains) / CGFloat(total) + let sold = total - remains + let remainingCountSize = self.remainingCount.update( + transition: transition, + component: AnyComponent(RemainingCountComponent( + inactiveColor: environment.theme.list.itemBlocksBackgroundColor, + activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)], + inactiveTitle: environment.strings.Gift_Send_Remains(remains), + inactiveValue: "", + inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, + activeTitle: "", + activeValue: environment.strings.Gift_Send_Sold(sold), + activeTitleColor: .white, + badgeText: "", + badgePosition: position, + badgeGraphPosition: position, + invertProgress: true, + leftString: "", + groupingSeparator: environment.dateTimeFormat.groupingSeparator + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: remainingCountSize) + if let remainingCountView = self.remainingCount.view { + if remainingCountView.superview == nil { + self.scrollContentView.addSubview(remainingCountView) + } + transition.setFrame(view: remainingCountView, frame: remainingCountFrame) + } + contentHeight += remainingCountSize.height + contentHeight += 7.0 + + if starGift.flags.contains(.isAuction) { + let parsedString = parseMarkdownIntoAttributedString("50 gifts are dropped at varying intervals to the top 50 bidders by bid amount. [Learn more >]()", attributes: footerAttributes) + let auctionFooterText = NSMutableAttributedString(attributedString: parsedString) + + if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { + self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + if let range = auctionFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { + auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string)) + } + + let auctionFooterSize = self.auctionFooter.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(auctionFooterText), + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component, case let .starGift(gift, _) = component.subject, let controller = self.environment?.controller() else { + return + } + let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: .generic(gift), completion: nil) + controller.push(infoController) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0) + ) + let auctionFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: auctionFooterSize) + if let auctionFooterView = self.auctionFooter.view { + if auctionFooterView.superview == nil { + self.scrollContentView.addSubview(auctionFooterView) + } + transition.setFrame(view: auctionFooterView, frame: auctionFooterFrame) + } + contentHeight += auctionFooterSize.height + } + contentHeight += sectionSpacing + } + + + initialContentHeight = contentHeight + + if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { + self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) + } + + + var buttonIsEnabled = true + let buttonString: String + switch component.subject { + case let .premium(product): + if self.payWithStars, let starsPrice = product.starsPrice { + let amountString = presentationStringsFormattedNumber(Int32(starsPrice), presentationData.dateTimeFormat.groupingSeparator) + buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + } else { + let amountString = product.price + buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" + } + case let .starGift(starGift, _): + var finalPrice: Int64 = starGift.price + if self.includeUpgrade, let upgradePrice = starGift.upgradeStars { + finalPrice += upgradePrice + } + let amountString = presentationStringsFormattedNumber(Int32(finalPrice), presentationData.dateTimeFormat.groupingSeparator) + let buttonTitle = isSelfGift ? environment.strings.Gift_Send_Buy : environment.strings.Gift_Send_Send + buttonString = "\(buttonTitle) # \(amountString)" + if let availability = starGift.availability, availability.remains == 0 { + buttonIsEnabled = false + } + } + + var buttonTitleItems: [AnyComponentWithIdentity] = [] + + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.kern, value: 2.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + var buttonIsLoading = false + if let _ = self.giftAuction { + //TODO:localize + let buttonAttributedString = NSMutableAttributedString(string: "Place a Bid", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + buttonTitleItems.append(AnyComponentWithIdentity(id: "bid", component: AnyComponent( + MultilineTextComponent(text: .plain(buttonAttributedString)) + ))) + if let giftAuctionState = self.giftAuctionState { + switch giftAuctionState.auctionState { + case let .ongoing(_, _, _, _, _, nextDropDate, _, _): + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let dropTimeout = nextDropDate - currentTime + + let minutes = Int(dropTimeout / 60) + let seconds = Int(dropTimeout % 60) + + let rawString = environment.strings.Gift_Setup_NextDropIn + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] + var startIndex = rawString.startIndex + while true { + if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) { + if range.lowerBound != startIndex { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< range.lowerBound])))) + } + + startIndex = range.upperBound + if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) { + let controlString = rawString[range.upperBound ..< endRange.lowerBound] + if controlString == "m" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(minutes, minDigits: 2))) + } else if controlString == "s" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .number(seconds, minDigits: 2))) + } + + startIndex = endRange.upperBound + } + } else { + break + } + } + if startIndex != rawString.endIndex { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: AnyHashable(buttonAnimatedTitleItems.count), content: .text(String(rawString[startIndex ..< rawString.endIndex])))) + } + + buttonTitleItems.append(AnyComponentWithIdentity(id: "timer", component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers), + color: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), + items: buttonAnimatedTitleItems, + noDelay: true + )))) + case .finished: + buttonIsEnabled = false + } + } else { + buttonIsLoading = true + } + } else { + buttonTitleItems.append(AnyComponentWithIdentity(id: buttonString, component: AnyComponent( + MultilineTextComponent(text: .plain(buttonAttributedString)) + ))) + } + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 32.0) + let buttonHeight: CGFloat = 52.0 + let actionButtonSize = self.actionButton.update( + transition: .spring(duration: 0.2), + 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.9), + isShimmering: true + ), + content: AnyComponentWithIdentity( + id: AnyHashable("title"), + component: AnyComponent(VStack(buttonTitleItems, spacing: 1.0)) + ), + isEnabled: buttonIsEnabled, + displaysProgress: buttonIsLoading || self.inProgress, + action: { [weak self] in + self?.proceed() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: buttonHeight) + ) + var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height + + let bottomEdgeEffectHeight: CGFloat = bottomPanelHeight + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: availableSize.width, height: bottomEdgeEffectHeight)) + transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + self.bottomEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition) + if self.bottomEdgeEffectView.superview == nil { + self.containerView.addSubview(self.bottomEdgeEffectView) + } + + let actionButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize) + if let buttonView = self.actionButton.view { + if buttonView.superview == nil { + self.containerView.addSubview(buttonView) + } + buttonView.frame = actionButtonFrame + } + + bottomPanelHeight -= 1.0 + + contentHeight += bottomPanelHeight + initialContentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.maxY + 24.0 + + var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + if self.inputPanelExternalState.isEditing { + topInset = 0.0 + } + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 38.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.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 let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let bottomInset: CGFloat = contentHeight - 12.0 + + let layout = ContainerViewLayout( + size: availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) + } + + return availableSize + } } func makeView() -> View { - return View() + return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + 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) } } -public final class GiftSetupScreen: ViewControllerComponentContainer { +public class GiftSetupScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case premium(PremiumGiftProduct) case starGift(StarGift.Gift, Bool?) @@ -2025,6 +2050,9 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { private let context: AccountContext + private var didPlayAppearAnimation: Bool = false + private var isDismissed: Bool = false + public init( context: AccountContext, peerId: EnginePeer.Id, @@ -2038,30 +2066,48 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { peerId: peerId, subject: subject, completion: completion - ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + ), navigationBarAppearance: .none, theme: .default) - self.title = "" - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) - - self.scrollToTop = { [weak self] in - guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else { - return - } - componentView.scrollToTop() - } + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func cancelPressed() { - self.dismiss() + deinit { } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + + if let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View { + componentView.animateIn() + } + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } } } @@ -2139,3 +2185,15 @@ public final class PremiumGiftProduct: Equatable { return true } } + +func hasFirstResponder(_ view: UIView) -> Bool { + if view.isFirstResponder { + return true + } + for subview in view.subviews { + if hasFirstResponder(subview) { + return true + } + } + return false +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift index 51005703d2..716f24cf2f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -137,6 +137,7 @@ public class RemainingCountComponent: Component { self.activeContainer = UIView() self.activeContainer.clipsToBounds = true + self.activeContainer.layer.cornerRadius = 15.0 self.activeBackground = SimpleLayer() self.activeBackground.anchorPoint = CGPoint() @@ -335,7 +336,7 @@ public class RemainingCountComponent: Component { } if "".isEmpty { if component.invertProgress { - progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight))) + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition - 15.0, y: 0.0), size: CGSize(width: size.width - activityPosition + 15.0, height: lineHeight))) progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBoughtScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBoughtScreen.swift new file mode 100644 index 0000000000..0cb4fc05cd --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBoughtScreen.swift @@ -0,0 +1,675 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import AccountContext +import ViewControllerComponent +import TelegramCore +import SwiftSignalKit +import Display +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import ButtonComponent +import PlainButtonComponent +import Markdown +import BundleIconComponent +import TextFormat +import TelegramStringFormatting +import GlassBarButtonComponent +import GiftItemComponent +import EdgeEffect + +private final class GiftAuctionBoughtScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let gift: StarGift + + init( + context: AccountContext, + gift: StarGift + ) { + self.context = context + self.gift = gift + } + + static func ==(lhs: GiftAuctionBoughtScreenComponent, rhs: GiftAuctionBoughtScreenComponent) -> Bool { + 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) + } + } + + final class View: UIView, UIScrollViewDelegate { + 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 bottomEdgeEffectView: EdgeEffectView + + private let backgroundHandleView: UIImageView + + private let closeButton = ComponentView() + private let title = ComponentView() + private var itemsViews: [Int64: ComponentView] = [:] + private let actionButton = ComponentView() + + private var ignoreScrolling: Bool = false + + private var component: GiftAuctionBoughtScreenComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ViewControllerComponentContainer.Environment? + 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.bottomEdgeEffectView = EdgeEffectView() + + 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 + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + 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") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + 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 { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + 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) + } + + func animateIn() { + 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) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + self.bottomEdgeEffectView.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) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + self.bottomEdgeEffectView.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: GiftAuctionBoughtScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = environment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, 428.0) - environment.safeInsets.left * 2.0 + } + let sideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + 24.0 + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor + + var locations: [NSNumber] = [] + var colors: [CGColor] = [] + let numStops = 6 + for i in 0 ..< numStops { + let step = CGFloat(i) / CGFloat(numStops - 1) + locations.append(step as NSNumber) + colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) + } + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 75.0 + + let tableFont = Font.regular(15.0) + let tableBoldFont = Font.semibold(15.0) + + let tableTextColor = environment.theme.list.itemPrimaryTextColor + + let mockDate = Int32(Date().timeIntervalSince1970) + + for i in 0 ..< 3 { + let id = Int64(i) + let itemView: ComponentView + if let current = self.itemsViews[id] { + itemView = current + } else { + itemView = ComponentView() + self.itemsViews[id] = itemView + } + + var items: [TableComponent.Item] = [] + + + var giftSubject: GiftItemComponent.Subject? + if case let .generic(gift) = component.gift { + giftSubject = .starGift(gift: gift, price: "") + } + + if let giftSubject { + items.append(.init( + id: "header", + title: nil, + hasBackground: true, + component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: giftSubject, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Round #\(3 + i * 2)", font: tableBoldFont, textColor: tableTextColor))) + ) + ) + ], spacing: 1.0)) + )) + } + + items.append(.init( + id: "recipient", + title: "Recipient", + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil + ) + ), + action: { + + } + )) + )) + + items.append(.init( + id: "date", + title: "Date", + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: mockDate, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat), font: tableFont, textColor: tableTextColor)))) + )) + + let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: Int64(3531 + 1000 * i + 13 * i), nanos: 0), dateTimeFormat: environment.dateTimeFormat))" + let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) + let range = (valueAttributedString.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + items.append(.init( + id: "bid", + title: "Accepted Bid", + component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "stars", component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity( + id: AnyHashable("info"), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: "TOP \(10 + i)", + color: environment.theme.list.itemAccentColor + )), + action: { + + } + )) + ) + ], spacing: 4.0)) + )) + + let itemSize = itemView.update( + transition: transition, + component: AnyComponent( + TableComponent( + theme: environment.theme, + items: items + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize) + if let view = itemView.view { + if view.superview == nil { + self.scrollContentView.addSubview(view) + } + view.frame = itemFrame + } + contentHeight += itemSize.height + contentHeight += 20.0 + } + + 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) + 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) + } + transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) + + let closeButtonSize = self.closeButton.update( + transition: .immediate, + 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.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + let title = self.title + let actionButton = self.actionButton + + let titleText = "3 Items Bought" + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 26.0), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + initialContentHeight = contentHeight + + let buttonAttributedString = NSMutableAttributedString(string: environment.strings.Common_OK, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 54.0, sideInset: 32.0) + + let actionButtonSize = actionButton.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.9), + cornerRadius: 54.0 * 0.5 + ), + content: AnyComponentWithIdentity( + id: AnyHashable("ok"), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 54.0) + ) + + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, 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) + if self.topEdgeEffectView.superview == nil { + self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) + } + + var bottomPanelHeight = 13.0 + buttonInsets.bottom + actionButtonSize.height + + let bottomEdgeEffectHeight: CGFloat = bottomPanelHeight + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: availableSize.width, height: bottomEdgeEffectHeight)) + transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + self.bottomEdgeEffectView.update(content: environment.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition) + if self.bottomEdgeEffectView.superview == nil { + self.containerView.addSubview(self.bottomEdgeEffectView) + } + + let actionButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize) + bottomPanelHeight -= 1.0 + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.containerView.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + contentHeight += bottomPanelHeight + initialContentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.maxY + 24.0 + + 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: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.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 let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let bottomInset: CGFloat = contentHeight - 12.0 + + let layout = ContainerViewLayout( + size: availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) + } + + 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) + } +} + +public class GiftAuctionBoughtScreen: ViewControllerComponentContainer { + public final class TransitionOut { + public let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + } + + private let context: AccountContext + + private var didPlayAppearAnimation: Bool = false + private var isDismissed: Bool = false + + public init(context: AccountContext, gift: StarGift) { + self.context = context + + super.init(context: context, component: GiftAuctionBoughtScreenComponent( + context: context, + gift: gift + ), navigationBarAppearance: .none, theme: .default) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + + if let componentView = self.node.hostView.componentView as? GiftAuctionBoughtScreenComponent.View { + componentView.animateIn() + } + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? GiftAuctionBoughtScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift index f80f38f7dd..3cea3deddc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift @@ -33,6 +33,7 @@ private final class BadgeComponent: Component { let prefix: String? let title: String let subtitle: String? + let subtitleOnTop: Bool let color: UIColor init( @@ -40,12 +41,14 @@ private final class BadgeComponent: Component { prefix: String?, title: String, subtitle: String?, + subtitleOnTop: Bool, color: UIColor ) { self.theme = theme self.prefix = prefix self.title = title self.subtitle = subtitle + self.subtitleOnTop = subtitleOnTop self.color = color } @@ -62,6 +65,9 @@ private final class BadgeComponent: Component { if lhs.subtitle != rhs.subtitle { return false } + if lhs.subtitleOnTop != rhs.subtitleOnTop { + return false + } if lhs.color != rhs.color { return false } @@ -553,7 +559,7 @@ private final class PeerComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) - let amountFrame = CGRect(origin: CGPoint(x: availableSize.width - amountSize.width, y: floorToScreenPixels((size.height - amountSize.height) / 2.0)), size: titleSize) + let amountFrame = CGRect(origin: CGPoint(x: availableSize.width - amountSize.width, y: floorToScreenPixels((size.height - amountSize.height) / 2.0)), size: amountSize) if let amountView = self.amount.view { if amountView.superview == nil { self.addSubview(amountView) @@ -943,13 +949,7 @@ private final class GiftAuctionScreenComponent: Component { return Amount(sliderValue: sliderValue, minRealValue: self.minRealValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) } } - - enum PrivacyPeer: Equatable { - case account - case anonymous - case peer(EnginePeer) - } - + final class View: UIView, UIScrollViewDelegate { private let dimView: UIView private let containerView: UIView @@ -989,9 +989,7 @@ private final class GiftAuctionScreenComponent: Component { private var peersMap: [EnginePeer.Id: EnginePeer] = [:] private let actionButton = ComponentView() - - private let bottomOverscrollLimit: CGFloat - + private var ignoreScrolling: Bool = false private var component: GiftAuctionScreenComponent? @@ -999,9 +997,7 @@ private final class GiftAuctionScreenComponent: Component { private var isUpdating: Bool = false private var environment: ViewControllerComponentContainer.Environment? private var itemLayout: ItemLayout? - - private var topOffsetDistance: CGFloat? - + private var balance: StarsAmount? private var amount: Amount = Amount(realValue: 1, minRealValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true) @@ -1014,8 +1010,6 @@ private final class GiftAuctionScreenComponent: Component { private var badgePhysicsLink: SharedDisplayLinkDriver.Link? override init(frame: CGRect) { - self.bottomOverscrollLimit = 200.0 - self.dimView = UIView() self.containerView = UIView() @@ -1111,20 +1105,6 @@ private final class GiftAuctionScreenComponent: Component { } } - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - /*guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { - return - } - - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - topOffset = max(0.0, topOffset) - - if topOffset < topOffsetDistance { - targetContentOffset.pointee.y = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) - }*/ - } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil @@ -1642,11 +1622,14 @@ private final class GiftAuctionScreenComponent: Component { transition.setFrame(view: sliderBackgroundView, frame: sliderBackgroundFrame) var subtitle: String? - let badgeValue = self.amount.realValue + var badgeValue = self.amount.realValue + var subtitleOnTop = false if let myBidAmount = self.giftAuctionState?.myState.bidAmount { if self.amount.realValue > myBidAmount { + badgeValue = self.amount.realValue subtitle = "+\(badgeValue - Int(myBidAmount))" + subtitleOnTop = true } else if myBidAmount == self.amount.realValue { subtitle = "your bid" } @@ -1659,6 +1642,7 @@ private final class GiftAuctionScreenComponent: Component { prefix: nil, title: "\(badgeValue)", subtitle: subtitle, + subtitleOnTop: subtitleOnTop, color: sliderColor )), environment: {}, @@ -1898,7 +1882,8 @@ private final class GiftAuctionScreenComponent: Component { guard let self, let component = self.component else { return } - let giftController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: component.gift, completion: {}) + //let giftController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, gift: component.gift, completion: {}) + let giftController = GiftAuctionBoughtScreen(context: component.context, gift: component.gift) self.environment?.controller()?.push(giftController) } )), @@ -2122,264 +2107,7 @@ private final class GiftAuctionScreenComponent: Component { } self.topPeerItems = [:] } - -// if !reactData.topPeers.isEmpty { -// contentHeight += 3.0 -// -// if case .message = reactData.reactSubject { -// let topPeersLeftSeparator: SimpleLayer -// if let current = self.topPeersLeftSeparator { -// topPeersLeftSeparator = current -// } else { -// topPeersLeftSeparator = SimpleLayer() -// self.topPeersLeftSeparator = topPeersLeftSeparator -// self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) -// } -// -// let topPeersRightSeparator: SimpleLayer -// if let current = self.topPeersRightSeparator { -// topPeersRightSeparator = current -// } else { -// topPeersRightSeparator = SimpleLayer() -// self.topPeersRightSeparator = topPeersRightSeparator -// self.scrollContentView.layer.addSublayer(topPeersRightSeparator) -// } -// -// let topPeersTitleBackground: SimpleLayer -// if let current = self.topPeersTitleBackground { -// topPeersTitleBackground = current -// } else { -// topPeersTitleBackground = SimpleLayer() -// self.topPeersTitleBackground = topPeersTitleBackground -// self.scrollContentView.layer.addSublayer(topPeersTitleBackground) -// } -// -// let topPeersTitle: ComponentView -// if let current = self.topPeersTitle { -// topPeersTitle = current -// } else { -// topPeersTitle = ComponentView() -// self.topPeersTitle = topPeersTitle -// } -// -// topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor -// topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor -// -// let topPeersTitleSize = topPeersTitle.update( -// transition: .immediate, -// component: AnyComponent(MultilineTextComponent( -// text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white)) -// )), -// environment: {}, -// containerSize: CGSize(width: 300.0, height: 100.0) -// ) -// let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0) -// let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize) -// -// topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor -// topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5 -// transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame) -// -// let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize) -// if let topPeersTitleView = topPeersTitle.view { -// if topPeersTitleView.superview == nil { -// self.scrollContentView.addSubview(topPeersTitleView) -// } -// transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame) -// } -// -// let separatorY = topPeersBackgroundFrame.midY -// let separatorSpacing: CGFloat = 10.0 -// transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) -// transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) -// -// contentHeight += 60.0 -// } -// -// var mappedTopPeers = reactData.topPeers -// if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) { -// mappedTopPeers.remove(at: index) -// } -// -// var myCount = 0 -// if let myTopPeer = reactData.myTopPeer { -// myCount += myTopPeer.count -// } -// var myCountAddition = 0 -// if self.didChangeAmount { -// myCountAddition = Int(self.amount.realValue) -// } -// myCount += myCountAddition -// if myCount != 0 { -// var topPeer: EnginePeer? -// switch self.privacyPeer { -// case .anonymous: -// topPeer = nil -// case .account: -// topPeer = reactData.myPeer -// case let .peer(peer): -// topPeer = peer -// } -// -// mappedTopPeers.append(GiftAuctionScreen.TopPeer( -// randomIndex: -1, -// peer: topPeer, -// isMy: true, -// count: myCount -// )) -// } -// mappedTopPeers.sort(by: { $0.count > $1.count }) -// if mappedTopPeers.count > 3 { -// mappedTopPeers = Array(mappedTopPeers.prefix(3)) -// } -// -// var animateItems = false -// var itemPositionTransition = transition -// var itemAlphaTransition = transition -// if transition.userData(IsAdjustingAmountHint.self) != nil { -// animateItems = true -// itemPositionTransition = .spring(duration: 0.3) -// itemAlphaTransition = .easeInOut(duration: 0.15) -// } -// -// var validIds: [GiftAuctionScreen.TopPeer.Id] = [] -// var items: [(itemView: ComponentView, size: CGSize)] = [] -// for topPeer in mappedTopPeers { -// validIds.append(topPeer.id) -// -// let itemView: ComponentView -// if let current = self.topPeerItems[topPeer.id] { -// itemView = current -// } else { -// itemView = ComponentView() -// self.topPeerItems[topPeer.id] = itemView -// } -// -// let itemCountString = presentationStringsFormattedNumber(Int32(topPeer.count), environment.dateTimeFormat.groupingSeparator) -// -// var peerColor: UIColor = UIColor(rgb: 0xFFB10D) -// if case .liveStream = reactData.reactSubject { -// let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(topPeer.count)).color ?? .purple -// peerColor = StoryLiveChatMessageComponent.getMessageColor(color: color) -// } -// -// let itemSize = itemView.update( -// transition: .immediate, -// component: AnyComponent(PlainButtonComponent( -// content: AnyComponent(PeerComponent( -// context: component.context, -// theme: environment.theme, -// strings: environment.strings, -// peer: topPeer.peer, -// count: itemCountString, -// color: peerColor -// )), -// effectAlignment: .center, -// action: { [weak self] in -// guard let self, let component = self.component, let peer = topPeer.peer else { -// return -// } -// guard let controller = self.environment?.controller() else { -// return -// } -// guard let navigationController = controller.navigationController as? NavigationController else { -// return -// } -// var viewControllers = navigationController.viewControllers -// guard let index = viewControllers.firstIndex(where: { $0 === controller }) else { -// return -// } -// -// let context = component.context -// -// if case .user = peer { -// if let peerInfoController = context.sharedContext.makePeerInfoController( -// context: context, -// updatedPresentationData: nil, -// peer: peer._asPeer(), -// mode: .generic, -// avatarInitiallyExpanded: false, -// fromChat: false, -// requestsContext: nil -// ) { -// viewControllers.insert(peerInfoController, at: index) -// } -// } else { -// let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) -// viewControllers.insert(chatController, at: index) -// } -// navigationController.setViewControllers(viewControllers, animated: true) -// controller.dismiss() -// }, -// isEnabled: topPeer.peer != nil && topPeer.peer?.id != component.context.account.peerId, -// animateAlpha: false -// )), -// environment: {}, -// containerSize: CGSize(width: 200.0, height: 200.0) -// ) -// items.append((itemView, itemSize)) -// } -// var removedIds: [GiftAuctionScreen.TopPeer.Id] = [] -// for (id, itemView) in self.topPeerItems { -// if !validIds.contains(id) { -// removedIds.append(id) -// -// if animateItems { -// if let itemComponentView = itemView.view { -// itemPositionTransition.setScale(view: itemComponentView, scale: 0.001) -// itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in -// itemComponentView?.removeFromSuperview() -// }) -// } -// } else { -// itemView.view?.removeFromSuperview() -// } -// } -// } -// for id in removedIds { -// self.topPeerItems.removeValue(forKey: id) -// } -// -// var itemsWidth: CGFloat = 0.0 -// for (_, itemSize) in items { -// itemsWidth += itemSize.width -// } -// -// let maxItemSpacing = 48.0 -// var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1)) -// itemSpacing = min(itemSpacing, maxItemSpacing) -// -// let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1) -// var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing -// for (itemView, itemSize) in items { -// if let itemComponentView = itemView.view { -// var animateItem = animateItems -// if itemComponentView.superview == nil { -// self.scrollContentView.addSubview(itemComponentView) -// animateItem = false -// ComponentTransition.immediate.setScale(view: itemComponentView, scale: 0.001) -// itemComponentView.alpha = 0.0 -// } -// -// let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight), size: itemSize) -// -// if animateItem { -// itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) -// itemPositionTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) -// } else { -// itemComponentView.center = itemFrame.center -// itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) -// } -// -// itemPositionTransition.setScale(view: itemComponentView, scale: 1.0) -// itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 1.0) -// } -// itemX += itemSize.width + itemSpacing -// } -// -// contentHeight += 104.0 -// } - + initialContentHeight = contentHeight if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { @@ -2388,15 +2116,19 @@ private final class GiftAuctionScreenComponent: Component { var formattedAmount = presentationStringsFormattedNumber(Int32(clamping: self.amount.realValue), environment.dateTimeFormat.groupingSeparator) let buttonString: String + let buttonId: String if let myBidAmount = self.giftAuctionState?.myState.bidAmount { if myBidAmount == self.amount.realValue { buttonString = environment.strings.Common_OK + buttonId = "ok" } else { formattedAmount = presentationStringsFormattedNumber(Int32(clamping: self.amount.realValue - Int(myBidAmount)), environment.dateTimeFormat.groupingSeparator) buttonString = "Add # \(formattedAmount) to Your Bid" + buttonId = "add" } } else { buttonString = "Place a # \(formattedAmount) Bid" + buttonId = "bid" } let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { @@ -2419,7 +2151,7 @@ private final class GiftAuctionScreenComponent: Component { cornerRadius: 54.0 * 0.5 ), content: AnyComponentWithIdentity( - id: AnyHashable(0), + id: AnyHashable(buttonId), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), isEnabled: true, @@ -2477,7 +2209,7 @@ private final class GiftAuctionScreenComponent: Component { contentHeight += bottomPanelHeight initialContentHeight += bottomPanelHeight - clippingY = actionButtonFrame.minY - 24.0 + clippingY = actionButtonFrame.maxY + 24.0 let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 6c70365a8a..f8faf8ca2c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -5498,7 +5498,7 @@ func formatPercentage(_ value: Float) -> String { return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%" } -private final class PeerCellComponent: Component { +final class PeerCellComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift index c7aaaa3ae9..eb9e874dd7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift @@ -70,6 +70,7 @@ final class TableComponent: CombinedComponent { } final class State: ComponentState { + var cachedLastBackgroundImage: (UIImage, PresentationTheme)? var cachedLeftColumnImage: (UIImage, PresentationTheme)? var cachedBorderImage: (UIImage, PresentationTheme)? } @@ -80,7 +81,7 @@ final class TableComponent: CombinedComponent { public static var body: Body { let leftColumnBackground = Child(Image.self) - let lastBackground = Child(Rectangle.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) @@ -130,7 +131,9 @@ final class TableComponent: CombinedComponent { var rowHeights: [Int: CGFloat] = [:] var totalHeight: CGFloat = 0.0 var innerTotalHeight: CGFloat = 0.0 - var hasLastBackground = false + var innerTotalOffset: CGFloat = 0.0 + var hasRowBackground = false + var rowBackgroundIsLast = false for item in context.component.items { let insets: UIEdgeInsets @@ -164,6 +167,8 @@ final class TableComponent: CombinedComponent { totalHeight += rowHeight if titleHeight > 0.0 { innerTotalHeight += rowHeight + } else if i == 0 { + innerTotalOffset += rowHeight } if i < context.component.items.count - 1 { @@ -176,39 +181,76 @@ final class TableComponent: CombinedComponent { } if item.hasBackground { - hasLastBackground = true + if i != 0 { + rowBackgroundIsLast = true + } + hasRowBackground = true } i += 1 } - if hasLastBackground { - let lastRowHeight = rowHeights[i - 1] ?? 0 + 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: Rectangle(color: secondaryBackgroundColor), + 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: totalHeight - lastRowHeight / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: position)) ) } - let borderRadius: CGFloat = 10.0 let leftColumnImage: UIImage if let (currentImage, theme) = context.state.cachedLeftColumnImage, theme === context.component.theme { leftColumnImage = currentImage } else { - leftColumnImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: CGSize(width: size.width + borderRadius, height: size.height)) + 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 hasRowBackground { + offset = rowBackgroundIsLast ? borderRadius : -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: 10, topCapHeight: 10) + })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) context.state.cachedLeftColumnImage = (leftColumnImage, context.component.theme) } @@ -218,14 +260,14 @@ final class TableComponent: CombinedComponent { transition: context.transition ) context.add(leftColumnBackground - .position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0)) + .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: 24.0, height: 24.0), rotatedContext: { size, context in + 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) @@ -239,7 +281,7 @@ final class TableComponent: CombinedComponent { context.setLineWidth(borderWidth) context.addPath(path) context.strokePath() - })!.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + })!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius)) context.state.cachedBorderImage = (borderImage, context.component.theme) } @@ -259,7 +301,7 @@ final class TableComponent: CombinedComponent { ) context.add( verticalBorder - .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalHeight / 2.0)) + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalOffset + innerTotalHeight / 2.0)) ) i = 0 @@ -275,7 +317,7 @@ final class TableComponent: CombinedComponent { ) valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) } else { - if hasLastBackground { + 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) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index ec7b716d98..754838f53b 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -51,7 +51,8 @@ public final class MessageInputPanelComponent: Component { case story case editor case media - case glass + case videoChat + case gift } public enum InputMode: Hashable { @@ -698,7 +699,7 @@ public final class MessageInputPanelComponent: Component { } } - public func getSendMessageInput() -> SendMessageInput { + public func getSendMessageInput(applyAutocorrection: Bool = true) -> SendMessageInput { if let inputPanelView = self.inputPanel?.view as? ChatTextInputPanelComponent.View { let _ = inputPanelView return .text(expandedInputStateAttributedString(self.textInputPanelExternalState.textInputState.inputText)) @@ -708,7 +709,7 @@ public final class MessageInputPanelComponent: Component { return .text(NSAttributedString()) } - return .text(textFieldView.getAttributedText()) + return .text(textFieldView.getAttributedText(applyAutocorrection: applyAutocorrection)) } public func setSendMessageInput(value: SendMessageInput, updateState: Bool) { @@ -1121,14 +1122,17 @@ public final class MessageInputPanelComponent: Component { let textFieldSideInset: CGFloat switch component.style { - case .media, .glass: + case .media, .videoChat, .gift: textFieldSideInset = 8.0 default: textFieldSideInset = 9.0 } var mediaInsets = UIEdgeInsets(top: insets.top, left: textFieldSideInset, bottom: insets.bottom, right: 41.0) - if case .glass = component.style { + if case .gift = component.style { + mediaInsets.right = textFieldSideInset + } + if case .videoChat = component.style { mediaInsets.right = 54.0 } @@ -1179,7 +1183,25 @@ public final class MessageInputPanelComponent: Component { let availableTextFieldSize = CGSize(width: availableSize.width - insets.left - insets.right, height: availableSize.height - insets.top - insets.bottom) + var formatMenuAvailability: TextFieldComponent.FormatMenuAvailability = .available(TextFieldComponent.FormatMenuAvailability.Action.all) + if component.isFormattingLocked { + formatMenuAvailability = .locked + } else if [.videoChat, .gift].contains(component.style) { + formatMenuAvailability = .available([.bold, .italic, .strikethrough, .underline, .spoiler]) + } self.textField.parentState = state + + let textColor: UIColor + let accentColor: UIColor + switch component.style { + case .gift: + textColor = component.theme.chat.inputPanel.inputTextColor + accentColor = component.theme.chat.inputPanel.inputTextColor + default: + textColor = UIColor(rgb: 0xffffff) + accentColor = UIColor(rgb: 0xffffff) + } + let textFieldSize = self.textField.update( transition: .immediate, component: AnyComponent(TextFieldComponent( @@ -1188,8 +1210,8 @@ public final class MessageInputPanelComponent: Component { strings: component.strings, externalState: self.textFieldExternalState, fontSize: 17.0, - textColor: UIColor(rgb: 0xffffff), - accentColor: UIColor(rgb: 0xffffff), + textColor: textColor, + accentColor: accentColor, insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0), hideKeyboard: component.hideKeyboard, customInputView: component.customInputView, @@ -1200,9 +1222,9 @@ public final class MessageInputPanelComponent: Component { } }, isOneLineWhenUnfocused: component.style == .media, - emptyLineHandling: component.style == .glass ? .notAllowed : .allowed, - formatMenuAvailability: component.isFormattingLocked ? .locked : .available(component.style == .glass ? [.bold, .italic, .strikethrough, .underline, .spoiler] : TextFieldComponent.FormatMenuAvailability.Action.all), - returnKeyType: component.style == .glass ? .send : .default, + emptyLineHandling: [.videoChat, .gift].contains(component.style) ? .notAllowed : .allowed, + formatMenuAvailability: formatMenuAvailability, + returnKeyType: [.videoChat, .gift].contains(component.style) ? .send : .default, lockedFormatAction: { component.presentTextFormattingTooltip?() }, @@ -1212,7 +1234,7 @@ public final class MessageInputPanelComponent: Component { paste: { data in component.paste(data) }, - returnKeyAction: component.style == .glass ? { [weak self] in + returnKeyAction: [.videoChat, .gift].contains(component.style) ? { [weak self] in self?.sendMessageAction() } : nil )), @@ -1224,6 +1246,10 @@ public final class MessageInputPanelComponent: Component { let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate let placeholderSize: CGSize + var placeholderColor = UIColor(rgb: 0xffffff, alpha: 0.4) + if case .gift = component.style { + placeholderColor = component.theme.chat.inputPanel.inputPlaceholderColor + } if case let .plain(string) = component.placeholder, string.contains("#") { let placeholderType = false if let currentPlaceholderType = self.currentPlaceholderType, currentPlaceholderType != placeholderType { @@ -1234,11 +1260,11 @@ public final class MessageInputPanelComponent: Component { self.vibrancyPlaceholder = ComponentView() } self.currentPlaceholderType = placeholderType - - let attributedPlaceholder = NSMutableAttributedString(string: string, font:Font.regular(17.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.4)) + + let attributedPlaceholder = NSMutableAttributedString(string: string, font:Font.regular(17.0), textColor: placeholderColor) if let range = attributedPlaceholder.string.range(of: "#") { attributedPlaceholder.addAttribute(.attachment, value: PresentationResourcesChat.chatPlaceholderStarIcon(component.theme)!, range: NSRange(range, in: attributedPlaceholder.string)) - attributedPlaceholder.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff, alpha: 0.4), range: NSRange(range, in: attributedPlaceholder.string)) + attributedPlaceholder.addAttribute(.foregroundColor, value: placeholderColor, range: NSRange(range, in: attributedPlaceholder.string)) attributedPlaceholder.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedPlaceholder.string)) } @@ -1292,7 +1318,7 @@ public final class MessageInputPanelComponent: Component { transition: placeholderTransition, component: AnyComponent(AnimatedTextComponent( font: Font.regular(17.0), - color: UIColor(rgb: 0xffffff, alpha: 0.4), + color: placeholderColor, items: placeholderItems )), environment: {}, @@ -1379,7 +1405,7 @@ public final class MessageInputPanelComponent: Component { var fieldBackgroundFrame: CGRect if hasMediaRecording { fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height)) - } else if case .glass = component.style { + } else if [.videoChat, .gift].contains(component.style) { fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height)) } else if isEditing || component.style == .editor || component.style == .media { fieldBackgroundFrame = fieldFrame @@ -1399,7 +1425,20 @@ public final class MessageInputPanelComponent: Component { //transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) switch component.style { - case .glass: + case .gift: + if self.fieldGlassBackgroundView == nil { + let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame) + self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView) + self.fieldGlassBackgroundView = fieldGlassBackgroundView + + self.fieldBackgroundView.isHidden = true + self.fieldBackgroundTint.isHidden = true + } + if let fieldGlassBackgroundView = self.fieldGlassBackgroundView { + fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) + transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame) + } + case .videoChat: if self.fieldGlassBackgroundView == nil { let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame) self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView) @@ -1437,7 +1476,7 @@ public final class MessageInputPanelComponent: Component { transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0) let placeholderOriginX: CGFloat - if isEditing || component.style == .story || component.style == .glass { + if isEditing || component.style == .story || component.style == .videoChat || component.style == .gift { placeholderOriginX = 16.0 } else { placeholderOriginX = floorToScreenPixels(fieldBackgroundFrame.minX + (fieldBackgroundFrame.width - placeholderSize.width) / 2.0) @@ -1657,7 +1696,7 @@ public final class MessageInputPanelComponent: Component { containerSize: availableTextFieldSize ) var counterFrame = CGRect(origin: CGPoint(x: availableSize.width - insets.right + floorToScreenPixels((insets.right - counterSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight - counterSize.height - 5.0), size: counterSize) - if case .glass = component.style { + if case .videoChat = component.style { counterFrame.origin.x -= 7.0 } if let counterView = self.counter.view { @@ -1836,7 +1875,10 @@ public final class MessageInputPanelComponent: Component { var inputActionButtonAvailableSize = CGSize(width: 33.0, height: 33.0) var inputActionButtonAlpha = 1.0 let inputActionButtonMode: MessageInputActionButtonComponent.Mode - if case .editor = component.style { + if case .gift = component.style { + inputActionButtonAlpha = 0.0 + inputActionButtonMode = .apply + } else if case .editor = component.style { if isEditing { inputActionButtonMode = .apply } else { @@ -1847,7 +1889,7 @@ public final class MessageInputPanelComponent: Component { if !isEditing { inputActionButtonAlpha = 0.0 } - } else if case .glass = component.style { + } else if case .videoChat = component.style { inputActionButtonAvailableSize = CGSize(width: 40.0, height: 40.0) if self.textFieldExternalState.hasText { inputActionButtonMode = .send @@ -1876,7 +1918,7 @@ public final class MessageInputPanelComponent: Component { } } let inputActionButtonStyle: MessageInputActionButtonComponent.Style - if component.style == .glass { + if component.style == .videoChat { inputActionButtonStyle = .glass(isTinted: true) } else if component.style == .story { inputActionButtonStyle = .legacy @@ -2008,9 +2050,9 @@ public final class MessageInputPanelComponent: Component { inputActionButtonOriginX -= 46.0 } } else { - if component.setMediaRecordingActive != nil || isEditing || component.style == .glass { + if component.setMediaRecordingActive != nil || isEditing || component.style == .videoChat { switch component.style { - case .glass: + case .videoChat: inputActionButtonOriginX = fieldBackgroundFrame.maxX + 6.0 default: inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5) @@ -2162,12 +2204,18 @@ public final class MessageInputPanelComponent: Component { animationName = "" } + let stickerButtonColor: UIColor + if case .gift = component.style { + stickerButtonColor = component.theme.chat.inputPanel.panelControlColor + } else { + stickerButtonColor = .white + } let stickerButtonSize = self.stickerButton.update( transition: transition, component: AnyComponent(Button( content: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: animationName), - color: .white + color: stickerButtonColor )), action: { [weak self] in guard let self else { @@ -2201,7 +2249,6 @@ public final class MessageInputPanelComponent: Component { } } - let accentColor = component.theme.chat.inputPanel.panelControlAccentColor if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue { let timeoutButtonSize = self.timeoutButton.update( transition: transition, @@ -2209,7 +2256,7 @@ public final class MessageInputPanelComponent: Component { content: AnyComponent( TimeoutContentComponent( color: .white, - accentColor: accentColor, + accentColor: component.theme.chat.inputPanel.panelControlAccentColor, isSelected: component.timeoutSelected, value: timeoutValue ) diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift index 3a5aabd841..84a436a1c8 100644 --- a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift +++ b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift @@ -116,7 +116,7 @@ public final class NavigationStackComponent: Compon super.init(frame: frame) self.dimView.alpha = 0.0 - self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.3) + self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2) self.dimView.isUserInteractionEnabled = false self.addSubview(self.dimView) } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 4bd16488ac..3b76429a5f 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -1013,8 +1013,10 @@ public final class TextFieldComponent: Component { return self.inputState } - public func getAttributedText() -> NSAttributedString { - Keyboard.applyAutocorrection(textView: self.textView) + public func getAttributedText(applyAutocorrection: Bool = true) -> NSAttributedString { + if applyAutocorrection { + Keyboard.applyAutocorrection(textView: self.textView) + } return expandedInputStateAttributedString(self.inputState.inputText) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5bb882de37..ad4508bd99 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3697,7 +3697,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let message = message else { return } - strongSelf.presentScheduleTimePicker(selectedTime: message.timestamp, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(selectedTime: message.timestamp, selectedRepeatPeriod: message._asMessage().scheduleRepeatPeriod, completion: { [weak self] time, repeatPeriod in if let strongSelf = self { var entities: TextEntitiesMessageAttribute? for attribute in message.attributes { @@ -3706,9 +3706,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - - let inlineStickers: [MediaId: TelegramMediaFile] = [:] - strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute(scheduleTime: time, repeatPeriod: repeatPeriod)) |> deliverOnMainQueue).startStrict(next: { result in + strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: [:], webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute(scheduleTime: time, repeatPeriod: repeatPeriod)) |> deliverOnMainQueue).startStrict(next: { result in }, error: { error in })) } @@ -9742,7 +9740,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) } - func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32, Int32?) -> Void) { + func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32, Int32?) -> Void) { guard let peerId = self.chatLocation.peerId else { return } @@ -9770,14 +9768,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = ChatScheduleTimeScreen( context: strongSelf.context, mode: mode, + currentTime: selectedTime, + currentRepeatPeriod: selectedRepeatPeriod, + minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, + isDark: style == .media, completion: { result in completion(result.time, result.repeatPeriod) } ) - -// let controller = ChatScheduleTimeController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: mode, style: style, currentTime: selectedTime, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, dismissByTapOutside: dismissByTapOutside, completion: { time in -// completion(time) -// }) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(controller, in: .window(.root)) }) diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index 8a604a0215..a2029d41cb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -346,7 +346,7 @@ extension ChatControllerImpl { }) })) } - + func beginDeleteMessagesWithUndo(messageIds: Set, type: InteractiveMessagesDeletionType) { var deleteImmediately = false if case .forEveryone = type { @@ -429,6 +429,68 @@ extension ChatControllerImpl { return } + if messageIds.count == 1, let message = messages.values.compactMap({ $0 }).first, let repeatAttribute = message.attributes.first(where: { $0 is ScheduledRepeatAttribute }) as? ScheduledRepeatAttribute { + let commit = { [weak self] in + guard let self else { + return + } + let title: String + let text: String + let deleteOneAction: String + let deleteAllAction: String + //TODO:localize + if message.id.peerId == self.context.account.peerId { + title = "Delete Repeating Reminder" + text = "Are you sure you want to delete this reminder? This is a repeating reminder." + deleteOneAction = "Delete This Reminder Only" + deleteAllAction = "Delete All Future Reminders" + } else { + title = "Delete Repeating Message" + text = "Are you sure you want to delete this scheduled message? This is a repeating message." + deleteOneAction = "Delete This Message Only" + deleteAllAction = "Delete All Future Messages" + } + self.present(standardTextAlertController( + theme: AlertControllerTheme(presentationData: self.presentationData), + title: title, + text: text, + actions: [ + TextAlertAction(type: .destructiveAction, title: deleteOneAction, action: { [weak self] in + guard let self else { + return + } + var entities: TextEntitiesMessageAttribute? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute + break + } + } + let scheduleTime = message.timestamp + repeatAttribute.repeatPeriod + self.editMessageDisposable.set((self.context.engine.messages.requestEditMessage(messageId: message.id, text: message.text, media: .keep, entities: entities, inlineStickers: [:], webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: repeatAttribute.repeatPeriod)) |> deliverOnMainQueue).startStrict(next: { result in + }, error: { error in + })) + }), + TextAlertAction(type: .destructiveAction, title: deleteAllAction, action: { [weak self] in + guard let self else { + return + } + self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) + }), + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical, + parseMarkdown: true + ), in: .window(.root)) + } + if let contextController { + contextController.dismiss(completion: commit) + } else { + commit() + } + return + } + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 89a4ca3b7a..f4ce285373 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -829,7 +829,7 @@ func openResolvedUrlImpl( }) case .loginEmail: if let navigationController { - let controller = loginEmailSetupController(context: context, emailPattern: nil, navigationController: navigationController, completion: {}) + let controller = loginEmailSetupController(context: context, blocking: true, emailPattern: nil, navigationController: navigationController, completion: {}) navigationController.pushViewController(controller) } }