diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8e091cb4ba..95f37268bc 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15633,3 +15633,18 @@ Error: %8$@"; "MediaEditor.Audio.ShowMore" = "Show More"; "WebApp.AddToAttachmentTitle" = "Add to Attachment Menu"; + +"Gift.Upgrade.Wearable.Title" = "Wearable"; +"Gift.Upgrade.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses."; +"Gift.Upgrade.ViewAllVariants" = "View all variants"; + +"CocoonInfo.Title" = "COCOON"; +"CocoonInfo.Description" = "**Cocoon** (**Co**nfidential **Co**mpute **O**pen **N**etwork) handles AI tasks **safely** and **efficiently**."; +"CocoonInfo.Private.Title" = "Private"; +"CocoonInfo.Private.Text" = "No third party can access any data, such as translations, inside Cocoon."; +"CocoonInfo.Efficient.Title" = "Efficient"; +"CocoonInfo.Efficient.Text" = "Cocoon has allowed Telegram to reduce translation costs by 6x."; +"CocoonInfo.ForEveryone.Title" = "For Everyone"; +"CocoonInfo.ForEveryone.Text" = "Any developer can use Cocoon for AI features. Learn more at [@cocoon](telegram) or [cocoon.org](web)."; +"CocoonInfo.IntergrateInfo" = "Want to integrate Cocoon into your projects?\nReach out at [t.me/cocoon?direct]()"; +"CocoonInfo.Understood" = "Understood"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 978c9b3ecb..7d382b4a8d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1428,7 +1428,7 @@ public protocol SharedAccountContext: AnyObject { func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController - + func makeCocoonInfoScreen(context: AccountContext) -> ViewController func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController @available(iOS 13.0, *) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 2a102a4cf5..beb821f279 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1517,7 +1517,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { switch recognizer.state { case .began: if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { - let startX = itemView.frame.minX - 4.0 + let startX = itemView.frame.minX self.selectionGestureState = (startX, startX, itemId) self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) @@ -2077,7 +2077,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { let panelSize = CGSize(width: isSelecting ? textPanelWidth : layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight) let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) - liquidLensView.update(size: panelSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 0.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) + liquidLensView.update(size: CGSize(width: panelSize.width, height: panelSize.height), selectionOrigin: CGPoint(x: max(0.0, min(panelSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 3.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition)) transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: panelSize)) diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 51996c3d55..4229f74605 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -36,6 +36,8 @@ public func chatTextLinkEditController( AlertTextComponent(content: .plain(strings.TextFormat_AddLinkText(text).string)) ) )) + + var applyImpl: (() -> Void)? content.append(AnyComponentWithIdentity( id: "input", component: AnyComponent( @@ -43,11 +45,15 @@ public func chatTextLinkEditController( context: context, initialValue: link.flatMap { NSAttributedString(string: $0) }, placeholder: strings.TextFormat_AddLinkPlaceholder, + returnKeyType: .done, keyboardType: .URL, autocapitalizationType: .none, autocorrectionType: .no, isInitiallyFocused: true, - externalState: inputState + externalState: inputState, + returnKeyAction: { + applyImpl?() + } ) ) )) @@ -66,20 +72,23 @@ public func chatTextLinkEditController( actions: [ .init(title: strings.Common_Cancel), .init(title: strings.Common_Done, type: .default, action: { - let updatedLink = explicitUrl(inputState.value.string) - if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { - dismissImpl?() - apply(updatedLink) - } else if inputState.value.string.isEmpty { - dismissImpl?() - apply("") - } else { - inputState.animateError() - } + applyImpl?() }, autoDismiss: false) ], updatedPresentationData: effectiveUpdatedPresentationData ) + applyImpl = { + let updatedLink = explicitUrl(inputState.value.string) + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { + dismissImpl?() + apply(updatedLink) + } else if inputState.value.string.isEmpty { + dismissImpl?() + apply("") + } else { + inputState.animateError() + } + } dismissImpl = { [weak alertController] in alertController?.dismiss(completion: nil) } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index dbf823676a..2bb3470ff8 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -346,6 +346,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking } let titleColor: UIColor + let linkColor = presentationData.theme.list.itemAccentColor switch self.item.textColor { case .primary: titleColor = presentationData.theme.contextMenu.primaryColor @@ -365,6 +366,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking return ChatTextInputStateTextAttribute(type: .bold, range: entity.range) } else if case .Italic = entity.type { return ChatTextInputStateTextAttribute(type: .italic, range: entity.range) + } else if case .Url = entity.type { + return ChatTextInputStateTextAttribute(type: .textUrl(""), range: entity.range) } return nil }) @@ -375,9 +378,12 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking ], range: NSRange(location: 0, length: result.length)) for attribute in inputStateText.attributes { if case .bold = attribute.type { - result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(presentationData.listsFontSize.baseDisplaySize), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + result.addAttribute(NSAttributedString.Key.font, value: titleBoldFont, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } else if case .italic = attribute.type { result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + } else if case .textUrl = attribute.type { + result.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + result.addAttribute(NSAttributedString.Key.font, value: titleBoldFont, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } attributedText = result diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index 02ac8f6572..d359be83de 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -197,279 +197,6 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg } } -private final class PromptAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let text: String - private let titleFont: PromptControllerTitleFont - private let subtitle: String? - - private let textNode: ASTextNode - private let subtitleNode: ASTextNode? - let inputFieldNode: PromptInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, subtitle: String?, value: String?, placeholder: String?, characterLimit: Int, displayCharacterLimit: Bool) { - self.strings = strings - self.text = text - self.titleFont = titleFont - self.subtitle = subtitle - - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 2 - - if subtitle != nil { - let subtitleNode = ASTextNode() - subtitleNode.maximumNumberOfLines = 0 - self.subtitleNode = subtitleNode - } else { - self.subtitleNode = nil - } - - self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: placeholder ?? "", characterLimit: characterLimit, displayCharacterLimit: displayCharacterLimit) - self.inputFieldNode.text = value ?? "" - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.textNode) - if let subtitleNode = self.subtitleNode { - self.addSubnode(subtitleNode) - } - - self.addSubnode(self.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = true - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - -// self.inputFieldNode.textChanged = { [weak self] text in -// if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { -// lastNode.actionEnabled = !text.isEmpty -// } -// } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - var value: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - let titleFontValue: UIFont - switch self.titleFont { - case .regular: - titleFontValue = Font.regular(13.0) - case .bold: - titleFontValue = Font.semibold(17.0) - } - self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center) - - if let subtitle = self.subtitle, let subtitleNode = self.subtitleNode { - subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - } - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = CGSize() -// transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) -// origin.y += titleSize.height + 4.0 - - let textSize = self.textNode.measure(measureSize) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) - origin.y += textSize.height + 6.0 + spacing - - var subtitleSize: CGSize? - if let subtitleNode { - let subtitleSizeValue = subtitleNode.measure(measureSize) - subtitleSize = subtitleSizeValue - transition.updateFrame(node: subtitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSizeValue.width) / 2.0), y: origin.y), size: subtitleSizeValue)) - origin.y += subtitleSizeValue.height + 6.0 + spacing - } - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - if let subtitleSize { - contentWidth = max(contentWidth, subtitleSize.width) - } - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - var resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) - if let subtitleSize { - resultSize.height += subtitleSize.height + spacing - } - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} - public enum PromptControllerTitleFont { case regular case bold diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index f19a07ca6f..6aacf354f0 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -132,6 +132,7 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/SliderComponent", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift index 4d62514a07..88a33bcdba 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -7,465 +7,100 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping -import ActivityIndicator +import ComponentFlow +import AlertComponent +import AlertInputFieldComponent -private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - fileprivate let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode +public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, apply: @escaping (String?) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? + let inputState = AlertInputFieldComponent.ExternalState() - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - } + let doneIsEnabled: Signal = inputState.valueSignal + |> map { value in + return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String) { - self.theme = theme + let doneInProgressValuePromise = ValuePromise(false) + let doneInProgress = doneInProgressValuePromise.get() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.WebBrowser_Exceptions_Create_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.WebBrowser_Exceptions_Create_Text)) + ) + )) - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .URL - self.textInputNode.autocapitalizationType = .none - self.textInputNode.returnKeyType = .done - self.textInputNode.autocorrectionType = .no - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) - private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) - - var inProgress = false - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if self.inProgress { - return false - } - if text == "\n" { - self.complete?() - return false - } - - if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex { - let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) - let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) - - if domainMatches.count > 0, pathMatches.count == 0 { - return true - } else { - return false - } - } - - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.attributedText = nil - self.deactivateInput() - } -} - -private final class WebBrowserDomainAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - - private let titleNode: ASTextNode - private let textNode: ASTextNode - let activityIndicator: ActivityIndicator - let inputFieldNode: WebBrowserDomainInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) { - self.strings = strings - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 2 - - self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow) - self.activityIndicator.isHidden = true - - self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder) - self.inputFieldNode.text = "" - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - - self.addSubnode(self.inputFieldNode) - self.addSubnode(self.activityIndicator) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = false - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - - self.inputFieldNode.textChanged = { [weak self] text in - if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - lastNode.actionEnabled = !text.isEmpty - } - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - var link: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 4.0 - - let textSize = self.textNode.measure(measureSize) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) - origin.y += textSize.height + 6.0 + spacing - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) - transition.updateFrame(node: self.inputFieldNode, frame: inputFrame) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let activitySize = CGSize(width: 20.0, height: 20.0) - transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize)) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} - -public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, apply: @escaping (String?) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? + let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) + let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: nil, + placeholder: strings.WebBrowser_Exceptions_Create_Placeholder, + characterLimit: nil, + hasClearButton: true, + keyboardType: .URL, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState, + shouldChangeText: { updatedText in + guard let domainRegex, let pathRegex else { + return true + } + let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + if domainMatches.count > 0, pathMatches.count == 0 { + return true + } else { + return false + } + }, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) - var inProgress = false - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - if !inProgress { - dismissImpl?(true) - apply(nil) - } - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - if !inProgress { - applyImpl?() - } - })] - - let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions) - contentNode.complete = { - applyImpl?() + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - inProgress = true - contentNode.inputFieldNode.inProgress = true - contentNode.activityIndicator.isHidden = false - - let updatedLink = explicitUrl(contentNode.link) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Common_Done, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgress) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + let updatedLink = explicitUrl(inputState.value) if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { + doneInProgressValuePromise.set(true) apply(updatedLink) } else { - contentNode.animateError() + inputState.animateError() } } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index c66c0a9916..85fd21740e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -419,7 +419,7 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl }) dismissImpl = { [weak linkController] in linkController?.view.endEditing(true) - linkController?.dismissAnimated() + linkController?.dismiss(completion: nil) } controller?.present(linkController, in: .window(.root)) } diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index bad899d08f..c771b6f492 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -115,7 +115,6 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/BackButtonComponent", - "//submodules/TelegramUI/Components/AlertComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/DirectMediaImageCache", "//submodules/FastBlur", @@ -131,6 +130,8 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/PremiumUI", "//submodules/TelegramUI/Components/Calls/VideoChatMicButtonComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 5edc8d2731..3a62b8af02 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -508,7 +508,7 @@ public final class MediaStreamComponent: CombinedComponent { let title: String = presentationData.strings.LiveStream_EditTitle let text: String = presentationData.strings.LiveStream_EditTitleText - let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in + let editController = voiceChatTitleEditController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in guard let call = call else { return } @@ -574,7 +574,7 @@ public final class MediaStreamComponent: CombinedComponent { title = presentationData.strings.LiveStream_StartRecordingTitle text = presentationData.strings.LiveStream_StartRecordingTextVideo - let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in + let editController = voiceChatTitleEditController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in guard let call = call, let controller = controller else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index e6b2071969..fcf96a12fb 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -691,7 +691,7 @@ final class VideoChatScreenComponent: Component { text = environment.strings.VoiceChat_EditTitleText } - let controller = voiceChatTitleEditController(sharedContext: groupCall.accountContext.sharedContext, account: groupCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in + let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index e77e72bf50..fed8bd66dc 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -469,7 +469,7 @@ extension VideoChatScreenComponent.View { } } - let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in + let controller = voiceChatTitleEditController(context: currentCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let peer = self.peer, let title else { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index dff33a3bfa..3b03188797 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -117,7 +117,7 @@ extension VideoChatScreenComponent.View { } else { maxBioLength = 100 } - let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in + let controller = voiceChatTitleEditController(context: currentCall.accountContext, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in guard let self, let environment = self.environment, let currentCall = self.currentCall, let bio else { return } @@ -149,7 +149,7 @@ extension VideoChatScreenComponent.View { guard let self, let environment = self.environment, let currentCall = self.currentCall else { return } - let controller = voiceChatUserNameController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in + let controller = voiceChatUserNameController(context: currentCall.accountContext, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in guard let self, let environment = self.environment, let currentCall = self.currentCall, let (firstName, lastName) = firstAndLastName else { return } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift index 1ba276b3db..505fb7d28a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTitleEditController.swift @@ -7,767 +7,179 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping +import ComponentFlow +import AlertComponent +import AlertInputFieldComponent -private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode - private let clearButton: HighlightableButtonNode - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - if self.textInputNode.isFirstResponder() { - self.clearButton.isHidden = newValue.isEmpty - } else { - self.clearButton.isHidden = true - } - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - private let maxLength: Int - - init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) { - self.theme = theme - self.maxLength = maxLength - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .default - self.textInputNode.autocapitalizationType = .sentences - self.textInputNode.returnKeyType = returnKeyType - self.textInputNode.autocorrectionType = .default - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - self.clearButton = HighlightableButtonNode() - self.clearButton.imageNode.displaysAsynchronously = false - self.clearButton.imageNode.displayWithoutProcessing = true - self.clearButton.displaysAsynchronously = false - self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) - self.clearButton.isHidden = true - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - self.addSubnode(self.clearButton) - - self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height))) - - if let image = self.clearButton.image(for: []) { - transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) - } - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - self.clearButton.isHidden = !self.placeholderNode.isHidden - } - - func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty - } - - func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - self.clearButton.isHidden = true - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) - if updatedText.count > maxLength { - self.textInputNode.layer.addShakeAnimation() - return false - } - if text == "\n" { - self.complete?() - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.placeholderNode.isHidden = false - self.clearButton.isHidden = true - - self.textInputNode.attributedText = nil - self.updateHeight?() - } -} - -private final class VoiceChatTitleEditAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - private let text: String - - private let titleNode: ASTextNode - private let textNode: ASTextNode - let inputFieldNode: VoiceChatTitleEditInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) { - self.strings = strings - self.title = title - self.text = text - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 8 - - self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength) - self.inputFieldNode.text = value ?? "" - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - - self.addSubnode(self.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) - } - } - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - var value: String { - return self.inputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 4.0 - - let textSize = self.textNode.measure(measureSize) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) - origin.y += textSize.height + 6.0 + spacing - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} - -func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController { - var presentationData = sharedContext.currentPresentationData.with { $0 } - if let forceTheme = forceTheme { +func voiceChatTitleEditController( + context: AccountContext, + forceTheme: PresentationTheme?, + title: String, + text: String, + placeholder: String, + doneButtonTitle: String? = nil, + value: String?, + maxLength: Int, + apply: @escaping (String?) -> Void +) -> ViewController { + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - - var dismissImpl: ((Bool) -> Void)? + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength) - contentNode.complete = { - applyImpl?() - } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - dismissImpl?(true) - + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: value, + placeholder: placeholder, + characterLimit: maxLength, + hasClearButton: true, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: doneButtonTitle ?? strings.Common_Done, type: .default, action: { + applyImpl?() + }) + ], + updatedPresentationData: (presentationData, .single(presentationData)) + ) + applyImpl = { let previousValue = value ?? "" - let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines) + let newValue = inputState.value.trimmingCharacters(in: .whitespacesAndNewlines) apply(previousValue != newValue || value == nil ? newValue : nil) } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in - var presentationData = presentationData - if let forceTheme = forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } -private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode { - private let strings: PresentationStrings - private let title: String - - private let titleNode: ASTextNode - let firstNameInputFieldNode: VoiceChatTitleEditInputFieldNode - let lastNameInputFieldNode: VoiceChatTitleEditInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - self.lastNameInputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, firstNameValue: String?, lastNameValue: String?, maxLength: Int) { - self.strings = strings - self.title = title - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - - self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next) - self.firstNameInputFieldNode.text = firstNameValue ?? "" - - self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength) - self.lastNameInputFieldNode.text = lastNameValue ?? "" - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - - self.addSubnode(self.firstNameInputFieldNode) - self.addSubnode(self.lastNameInputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - - self.firstNameInputFieldNode.complete = { [weak self] in - self?.lastNameInputFieldNode.activateInput() - } - } - - deinit { - self.disposable.dispose() - } - - var firstName: String { - return self.firstNameInputFieldNode.text - } - - var lastName: String { - return self.lastNameInputFieldNode.text - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 0.0 - - let titleSize = self.titleNode.measure(measureSize) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 4.0 - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let firstInputFieldHeight = self.firstNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - transition.updateFrame(node: self.firstNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: firstInputFieldHeight)) - - origin.y += firstInputFieldHeight + spacing - - let lastInputFieldHeight = self.lastNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - transition.updateFrame(node: self.lastNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: lastInputFieldHeight)) - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + firstInputFieldHeight + spacing + lastInputFieldHeight + actionsHeight + insets.top + insets.bottom) - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if !hadValidLayout { - self.firstNameInputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - if self.firstNameInputFieldNode.text.isEmpty { - self.firstNameInputFieldNode.layer.addShakeAnimation() - } - self.hapticFeedback.error() - } -} - -func voiceChatUserNameController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, doneButtonTitle: String? = nil, firstName: String?, lastName: String?, maxLength: Int, apply: @escaping ((String, String)?) -> Void) -> AlertController { - var presentationData = sharedContext.currentPresentationData.with { $0 } - if let forceTheme = forceTheme { +func voiceChatUserNameController( + context: AccountContext, + forceTheme: PresentationTheme?, + title: String, + firstNamePlaceholder: String, + lastNamePlaceholder: String, + doneButtonTitle: String? = nil, + firstName: String?, + lastName: String?, + maxLength: Int, + apply: @escaping ((String, String)?) -> Void +) -> ViewController { + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } + let strings = presentationData.strings + + let firstNameState = AlertInputFieldComponent.ExternalState() + let lastNameState = AlertInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + )) - var dismissImpl: ((Bool) -> Void)? + var nextImpl: (() -> Void)? var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "firstName", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: firstName, + placeholder: firstNamePlaceholder, + characterLimit: maxLength, + hasClearButton: true, + returnKeyType: .next, + isInitiallyFocused: true, + externalState: firstNameState, + returnKeyAction: { + nextImpl?() + } + ) + ) + )) - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = VoiceChatUserNameEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, firstNamePlaceholder: firstNamePlaceholder, lastNamePlaceholder: lastNamePlaceholder, firstNameValue: firstName, lastNameValue: lastName, maxLength: maxLength) - contentNode.complete = { - applyImpl?() + content.append(AnyComponentWithIdentity( + id: "lastName", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: lastName, + placeholder: lastNamePlaceholder, + characterLimit: maxLength, + hasClearButton: true, + isInitiallyFocused: false, + externalState: lastNameState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: doneButtonTitle ?? strings.Common_Done, type: .default, action: { + applyImpl?() + }) + ], + updatedPresentationData: (presentationData, .single(presentationData)) + ) + nextImpl = { + lastNameState.activateInput() } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - + applyImpl = { let previousFirstName = firstName ?? "" let previousLastName = lastName ?? "" - let newFirstName = contentNode.firstName.trimmingCharacters(in: .whitespacesAndNewlines) - let newLastName = contentNode.lastName.trimmingCharacters(in: .whitespacesAndNewlines) + let newFirstName = firstNameState.value.trimmingCharacters(in: .whitespacesAndNewlines) + let newLastName = lastNameState.value.trimmingCharacters(in: .whitespacesAndNewlines) if newFirstName.isEmpty { - contentNode.animateError() + firstNameState.animateError() return } - - dismissImpl?(true) - + if previousFirstName != newFirstName || previousLastName != newLastName { apply((newFirstName, newLastName)) } else { apply(nil) } } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in - var presentationData = presentationData - if let forceTheme = forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.firstNameInputFieldNode.updateTheme(presentationData.theme) - contentNode?.lastNameInputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.firstNameInputFieldNode.deactivateInput() - contentNode?.lastNameInputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 083f6d1a3b..f413eb739b 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -511,6 +511,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiGameStakeScreen", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController", + "//submodules/TelegramUI/Components/CocoonInfoScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD new file mode 100644 index 0000000000..86dafdff82 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertInputFieldComponent", + module_name = "AlertInputFieldComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TextFormat", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift new file mode 100644 index 0000000000..902c0d20ff --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent/Sources/AlertInputFieldComponent.swift @@ -0,0 +1,328 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AlertComponent +import MultilineTextComponent +import AccountContext +import TextFormat +import PlainButtonComponent +import BundleIconComponent + +public final class AlertInputFieldComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public class ExternalState { + public fileprivate(set) var value: String = "" + public fileprivate(set) var animateError: () -> Void = {} + public fileprivate(set) var activateInput: () -> Void = {} + fileprivate let valuePromise = ValuePromise("") + public var valueSignal: Signal { + return self.valuePromise.get() + } + + public init() { + } + } + + let context: AccountContext + let initialValue: String? + let placeholder: String + let characterLimit: Int? + let hasClearButton: Bool + let isSecureTextEntry: Bool + let returnKeyType: UIReturnKeyType + let keyboardType: UIKeyboardType + let autocapitalizationType: UITextAutocapitalizationType + let autocorrectionType: UITextAutocorrectionType + let isInitiallyFocused: Bool + let externalState: ExternalState + let shouldChangeText: ((String) -> Bool)? + let returnKeyAction: (() -> Void)? + + public init( + context: AccountContext, + initialValue: String? = nil, + placeholder: String, + characterLimit: Int? = nil, + hasClearButton: Bool = false, + isSecureTextEntry: Bool = false, + returnKeyType: UIReturnKeyType = .done, + keyboardType: UIKeyboardType = .default, + autocapitalizationType: UITextAutocapitalizationType = .sentences, + autocorrectionType: UITextAutocorrectionType = .default, + isInitiallyFocused: Bool = false, + externalState: ExternalState, + shouldChangeText: ((String) -> Bool)? = nil, + returnKeyAction: (() -> Void)? = nil + ) { + self.context = context + self.initialValue = initialValue + self.placeholder = placeholder + self.characterLimit = characterLimit + self.hasClearButton = hasClearButton + self.isSecureTextEntry = isSecureTextEntry + self.returnKeyType = returnKeyType + self.keyboardType = keyboardType + self.autocapitalizationType = autocapitalizationType + self.autocorrectionType = autocorrectionType + self.isInitiallyFocused = isInitiallyFocused + self.externalState = externalState + self.shouldChangeText = shouldChangeText + self.returnKeyAction = returnKeyAction + } + + public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.characterLimit != rhs.characterLimit { + return false + } + if lhs.hasClearButton != rhs.hasClearButton { + return false + } + if lhs.isSecureTextEntry != rhs.isSecureTextEntry { + return false + } + if lhs.returnKeyType != rhs.returnKeyType { + return false + } + if lhs.keyboardType != rhs.keyboardType { + return false + } + if lhs.autocapitalizationType != rhs.autocapitalizationType { + return false + } + if lhs.autocorrectionType != rhs.autocorrectionType { + return false + } + if lhs.isInitiallyFocused != rhs.isInitiallyFocused { + return false + } + return true + } + + private final class TextField: UITextField { + var sideInset: CGFloat = 0.0 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) + } + } + + public final class View: UIView, UITextFieldDelegate { + private let background = ComponentView() + private let textField = TextField() + private let placeholder = ComponentView() + private let clearButton = ComponentView() + + private var component: AlertInputFieldComponent? + private weak var state: EmptyComponentState? + + private var isUpdating = false + + var currentText: String { + return self.textField.text ?? "" + } + + func activateInput() { + self.textField.becomeFirstResponder() + } + + func animateError() { + self.textField.layer.addShakeAnimation() + + HapticFeedback().error() + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.component?.returnKeyAction?() + return false + } + + @objc private func textDidChange() { + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + self.clearButton.view?.isHidden = self.currentText.isEmpty + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + self.clearButton.view?.isHidden = true + } + + public func setText(text: String) { + self.textField.text = text + if !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + } + + func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + var resetText: String? + if self.component == nil { + resetText = component.initialValue + component.externalState.animateError = { [weak self] in + self?.animateError() + } + component.externalState.activateInput = { [weak self] in + self?.activateInput() + } + } + + let isFirstTime = self.component == nil + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let topInset: CGFloat = 15.0 + + if self.textField.superview == nil { + self.addSubview(self.textField) + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) + } + if self.textField.autocapitalizationType != component.autocapitalizationType { + self.textField.autocapitalizationType = component.autocapitalizationType + } + if self.textField.autocorrectionType != component.autocorrectionType { + self.textField.autocorrectionType = component.autocorrectionType + } + if self.textField.isSecureTextEntry != component.isSecureTextEntry { + self.textField.isSecureTextEntry = component.isSecureTextEntry + } + if self.textField.returnKeyType != component.returnKeyType { + self.textField.returnKeyType = component.returnKeyType + } + self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light + if let resetText { + self.textField.text = resetText + } + + self.textField.font = Font.regular(17.0) + self.textField.textColor = environment.theme.actionSheet.primaryTextColor + self.textField.tintColor = environment.theme.actionSheet.controlAccentColor + self.textField.sideInset = 16.0 + + let backgroundPadding: CGFloat = 14.0 + let size = CGSize(width: availableSize.width, height: 50.0) + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false) + ), + environment: {}, + containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height) + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0) + let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize) + transition.setFrame(view: self.textField, frame: textFieldFrame) + + let placeholderSize = self.placeholder.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString( + string: component.placeholder, + font: Font.regular(17.0), + textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4) + ))) + ), + environment: {}, + containerSize: CGSize(width: size.width, height: 50.0) + ) + let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize) + if let placeholderView = self.placeholder.view { + if placeholderView.superview == nil { + placeholderView.isUserInteractionEnabled = false + self.addSubview(placeholderView) + } + placeholderView.frame = placeholderFrame + placeholderView.isHidden = !self.currentText.isEmpty + } + + if component.hasClearButton { + let clearButtonSize = self.clearButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Components/Search Bar/Clear", + tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4) + )), + effectAlignment: .center, + minSize: CGSize(width: 44.0, height: 44.0), + action: { [weak self] in + guard let self else { + return + } + self.setText(text: "") + }, + animateAlpha: false, + animateScale: true + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let clearButtonView = self.clearButton.view { + if clearButtonView.superview == nil { + self.addSubview(clearButtonView) + } + transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize)) + clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder + } + } else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil { + clearButtonView.removeFromSuperview() + } + + if isFirstTime && component.isInitiallyFocused { + self.activateInput() + } + + component.externalState.value = self.currentText + component.externalState.valuePromise.set(self.currentText) + + return CGSize(width: availableSize.width, height: size.height + topInset) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift index 93e6b133d9..49f9b1e7d7 100644 --- a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift @@ -18,6 +18,7 @@ public final class AlertMultilineInputFieldComponent: Component { public class ExternalState { public fileprivate(set) var value: NSAttributedString = NSAttributedString() public fileprivate(set) var animateError: () -> Void = {} + public fileprivate(set) var activateInput: () -> Void = {} fileprivate let valuePromise = ValuePromise(NSAttributedString()) public var valueSignal: Signal { return self.valuePromise.get() @@ -115,6 +116,7 @@ public final class AlertMultilineInputFieldComponent: Component { let placeholder: String let prefix: NSAttributedString? let characterLimit: Int? + let returnKeyType: UIReturnKeyType let keyboardType: UIKeyboardType let autocapitalizationType: UITextAutocapitalizationType let autocorrectionType: UITextAutocorrectionType @@ -131,6 +133,7 @@ public final class AlertMultilineInputFieldComponent: Component { placeholder: String, prefix: NSAttributedString? = nil, characterLimit: Int? = nil, + returnKeyType: UIReturnKeyType = .default, keyboardType: UIKeyboardType = .default, autocapitalizationType: UITextAutocapitalizationType = .sentences, autocorrectionType: UITextAutocorrectionType = .default, @@ -146,6 +149,7 @@ public final class AlertMultilineInputFieldComponent: Component { self.placeholder = placeholder self.prefix = prefix self.characterLimit = characterLimit + self.returnKeyType = returnKeyType self.keyboardType = keyboardType self.autocapitalizationType = autocapitalizationType self.autocorrectionType = autocorrectionType @@ -170,6 +174,9 @@ public final class AlertMultilineInputFieldComponent: Component { if lhs.prefix != rhs.prefix { return false } + if lhs.returnKeyType != rhs.returnKeyType { + return false + } if lhs.characterLimit != rhs.characterLimit { return false } @@ -203,6 +210,12 @@ public final class AlertMultilineInputFieldComponent: Component { private var component: AlertMultilineInputFieldComponent? private weak var state: EmptyComponentState? + func activateInput() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.activateInput() + } + } + func animateError() { if let textFieldView = self.textField.view { textFieldView.layer.addShakeAnimation() @@ -217,6 +230,9 @@ public final class AlertMultilineInputFieldComponent: Component { component.externalState.animateError = { [weak self] in self?.animateError() } + component.externalState.activateInput = { [weak self] in + self?.activateInput() + } } let isFirstTime = self.component == nil @@ -228,6 +244,7 @@ public final class AlertMultilineInputFieldComponent: Component { let topInset: CGFloat = 15.0 let horizontalInset: CGFloat = 4.0 + let verticalInset: CGFloat = 11.0 - UIScreenPixel let textFieldSize = self.textField.update( transition: transition, @@ -247,7 +264,7 @@ public final class AlertMultilineInputFieldComponent: Component { characterLimit: component.characterLimit, emptyLineHandling: component.emptyLineHandling.textFieldValue, formatMenuAvailability: component.formatMenuAvailability.textFieldValue, - returnKeyType: .done, + returnKeyType: component.returnKeyType, keyboardType: component.keyboardType, autocapitalizationType: component.autocapitalizationType, autocorrectionType: component.autocorrectionType, @@ -277,7 +294,7 @@ public final class AlertMultilineInputFieldComponent: Component { component.externalState.valuePromise.set(component.externalState.value) let backgroundPadding: CGFloat = 14.0 - let size = CGSize(width: availableSize.width, height: max(50.0, textFieldSize.height + 22.0)) + let size = CGSize(width: availableSize.width, height: max(50.0, floor(textFieldSize.height + verticalInset * 2.0))) let backgroundSize = self.background.update( transition: transition, @@ -327,9 +344,7 @@ public final class AlertMultilineInputFieldComponent: Component { } if isFirstTime && component.isInitiallyFocused { - if let textFieldView = self.textField.view as? TextFieldComponent.View { - textFieldView.activateInput() - } + self.activateInput() } return CGSize(width: availableSize.width, height: size.height + topInset) diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift index 4f330f6bcc..30cbac294a 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertActionComponent.swift @@ -161,17 +161,14 @@ final class AlertActionComponent: Component { activity = ComponentView() self.activity = activity } - let activitySize = activity.update( + let activitySize = CGSize(width: 18.0, height: 18.0) + let _ = activity.update( transition: transition, component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)), environment: {}, - containerSize: availableSize + containerSize: activitySize ) if let activityView = activity.view { - if activityView.superview == nil { - self.addSubview(activityView) - transition.animateAlpha(view: activityView, from: 0.0, to: 1.0) - } activityView.bounds = CGRect(origin: .zero, size: activitySize) } } else if let activity = self.activity { @@ -207,9 +204,16 @@ final class AlertActionComponent: Component { } if let activityView = self.activity?.view { + var activityTransition = transition + if activityView.superview == nil { + self.addSubview(activityView) + transition.animateAlpha(view: activityView, from: 0.0, to: 1.0) + activityTransition = .immediate + } let activitySize = activityView.bounds.size let activityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - activitySize.width) / 2.0), y: floorToScreenPixels((size.height - activitySize.height) / 2.0)), size: activitySize) - transition.setFrame(view: activityView, frame: activityFrame) + activityTransition.setPosition(view: activityView, position: activityFrame.center) + activityView.transform = CGAffineTransformMakeScale(0.7, 0.7) } } } diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index 8c82ff746e..efc2346143 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -373,7 +373,7 @@ private final class AlertScreenComponent: Component { let defaultActionTheme = AlertActionComponent.Theme( background: environment.theme.actionSheet.controlAccentColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, - secondary: environment.theme.actionSheet.secondaryTextColor, + secondary: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85), font: .bold ) let destructiveActionTheme = AlertActionComponent.Theme( diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD b/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD new file mode 100644 index 0000000000..014f566a64 --- /dev/null +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CocoonInfoScreen", + module_name = "CocoonInfoScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/TelegramStringFormatting", + "//submodules/TextFormat", + "//submodules/PresentationDataUtils", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift new file mode 100644 index 0000000000..61d04d1edf --- /dev/null +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift @@ -0,0 +1,669 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import Markdown +import TextFormat +import TelegramStringFormatting +import GlassBarButtonComponent +import ButtonComponent +import LottieComponent + +private final class CocoonInfoSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let animateOut: ActionSlot> + let getController: () -> ViewController? + + init( + context: AccountContext, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.animateOut = animateOut + self.getController = getController + } + + static func ==(lhs: CocoonInfoSheetContent, rhs: CocoonInfoSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let animateOut: ActionSlot> + private let getController: () -> ViewController? + + fileprivate let playButtonAnimation = ActionSlot() + private var didPlayAnimation = false + + init( + context: AccountContext, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.animateOut = animateOut + self.getController = getController + + super.init() + } + + func playAnimationIfNeeded() { + if !self.didPlayAnimation { + self.didPlayAnimation = true + self.playButtonAnimation.invoke(Void()) + } + } + + func dismiss(animated: Bool) { + guard let controller = self.getController() as? CocoonInfoScreen else { + return + } + if animated { + self.animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } else { + controller.dismiss(animated: false) + } + } + } + + func makeState() -> State { + return State(context: self.context, animateOut: self.animateOut, getController: self.getController) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let icon = Child(BundleIconComponent.self) + let title = Child(BalancedTextComponent.self) + let text = Child(BalancedTextComponent.self) + let list = Child(List.self) + let additionalText = Child(MultilineTextComponent.self) + let button = Child(ButtonComponent.self) + + let navigateDisposable = MetaDisposable() + + return { context in + let component = context.component + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let state = context.state + + let theme = environment.theme + let strings = environment.strings + + let sideInset: CGFloat = 30.0 + environment.safeInsets.left + let textSideInset: CGFloat = 30.0 + environment.safeInsets.left + + let titleFont = Font.bold(24.0) + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 28.0) + + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/Cocoon", tintColor: nil + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0)) + ) + contentSize.height += icon.size.height + contentSize.height += 14.0 + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: strings.CocoonInfo_Title, font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += spacing - 8.0 + + let attributedText = parseMarkdownIntoAttributedString( + strings.CocoonInfo_Description, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + linkAttribute: { _ in return nil } + ) + ) + let text = text.update( + component: BalancedTextComponent( + text: .plain(attributedText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + ) + contentSize.height += text.size.height + contentSize.height += spacing + 9.0 + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "private", + component: AnyComponent(ParagraphComponent( + title: strings.CocoonInfo_Private_Title, + titleColor: textColor, + text: strings.CocoonInfo_Private_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Ads/Privacy", + iconColor: linkColor, + action: { _, _ in + } + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "efficient", + component: AnyComponent(ParagraphComponent( + title: strings.CocoonInfo_Efficient_Title, + titleColor: textColor, + text: strings.CocoonInfo_Efficient_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Premium/Stats", + iconColor: linkColor, + action: { _, _ in + } + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "for_everyone", + component: AnyComponent(ParagraphComponent( + title: strings.CocoonInfo_ForEveryone_Title, + titleColor: textColor, + text: strings.CocoonInfo_ForEveryone_Text, + textColor: secondaryTextColor, + accentColor: linkColor, + iconName: "Chat/Input/Accessory Panels/Gift", + iconColor: linkColor, + action: { attributes, _ in + guard let link = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, let controller = environment.controller() else { + return + } + switch link { + case "telegram": + component.context.sharedContext.handleTextLinkAction(context: component.context, peerId: nil, navigateDisposable: navigateDisposable, controller: controller, action: .tap, itemLink: .url(url: "https://t.me/cocoon", concealed: false)) + case "web": + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: "https://cocoon.org", forceExternal: true, presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + default: + break + } + } + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += spacing - 6.0 + + let attributedAdditionalText = parseMarkdownIntoAttributedString( + strings.CocoonInfo_IntergrateInfo, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: secondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: secondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: linkColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + ) + let additionalText = additionalText.update( + component: MultilineTextComponent( + text: .plain(attributedAdditionalText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + guard let controller = environment.controller() else { + return + } + component.context.sharedContext.handleTextLinkAction(context: component.context, peerId: nil, navigateDisposable: navigateDisposable, controller: controller, action: .tap, itemLink: .url(url: "https://t.me/cocoon?direct", concealed: false)) + } + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + additionalText.size.height / 2.0)) + ) + contentSize.height += additionalText.size.height + contentSize.height += spacing + 6.0 + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak state] _ in + guard let state else { + return + } + state.dismiss(animated: true) + } + ), + availableSize: CGSize(width: 40.0, height: 40.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_ok"), + color: theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 28.0, height: 28.0), + playOnce: state.playButtonAnimation + )))) + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( + text: strings.CocoonInfo_Understood, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )))) + + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + guard let state else { + return + } + state.dismiss(animated: true) + } + ), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + + contentSize.height += 30.0 + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +final class CocoonInfoSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: CocoonInfoSheetComponent, rhs: CocoonInfoSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(CocoonInfoSheetContent( + context: context.component.context, + animateOut: animateOut, + getController: controller + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + autoAnimateOut: false, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: { + }, + willDismiss: { + } + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + if let controller = controller() as? CocoonInfoScreen { + animateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } + } else { + if let controller = controller() as? CocoonInfoScreen { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + var sideInset: CGFloat = 0.0 + var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight) + if case .regular = environment.metrics.widthClass { + sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0 + bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight + } + + let layout = ContainerViewLayout( + size: context.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: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + +public final class CocoonInfoScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext + ) { + self.context = context + + super.init( + context: context, + component: CocoonInfoSheetComponent( + context: context + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let accentColor: UIColor + let iconName: String + let iconColor: UIColor + let action: (([NSAttributedString.Key: Any], Int) -> Void)? + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + accentColor: UIColor, + iconName: String, + iconColor: UIColor, + action: (([NSAttributedString.Key: Any], Int) -> Void)? + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.accentColor = accentColor + self.iconName = iconName + self.iconColor = iconColor + self.action = action + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let leftInset: CGFloat = 32.0 + let rightInset: CGFloat = 24.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let accentColor = component.accentColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: accentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, index in + component.action?(attributes, index) + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 13132bc01b..ae2cebc6c4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -514,8 +514,8 @@ final class EntityKeyboardBottomPanelComponent: Component { lensSelection = (selectionGestureState.currentX, lensSelection.width) } - transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 0.0), size: tabsSize)) - self.liquidLensView.update(size: tabsSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), inset: 0.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: activeContentId == nil, transition: transition) + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabsSize.width) / 2.0), y: 3.0), size: tabsSize)) + self.liquidLensView.update(size: CGSize(width: tabsSize.width, height: tabsSize.height), selectionOrigin: CGPoint(x: max(0.0, min(tabsSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: tabsSize.height), inset: 3.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: activeContentId == nil, transition: transition) var removedIconViewIds: [AnyHashable] = [] for (id, iconView) in self.itemViews { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift index d9cf921a9a..5c7abf8318 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionCustomBidController.swift @@ -8,347 +8,287 @@ import TelegramPresentationData import AccountContext import UrlEscaping import ComponentFlow +import AlertComponent import StarsWithdrawalScreen -private final class GiftAuctionCustomBidAlertContentNode: AlertContentNode { - private let theme: PresentationTheme - private let strings: PresentationStrings - private let dateTimeFormat: PresentationDateTimeFormat - private let title: String - private let text: String - private let placeholder: String - private let minValue: Int64 - fileprivate var value: Int64 +func giftAuctionCustomBidController( + context: AccountContext, + title: String, + text: String, + placeholder: String, + action: String, + minValue: Int64, + value: Int64, + apply: @escaping (Int64) -> Void, + cancel: @escaping () -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings - private let titleNode: ASTextNode - private let textNode: ASTextNode - private let backgroundView = UIImageView() - let amountField = ComponentView() - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? { - didSet { - //self.inputFieldNode.complete = self.complete - } - } - - override var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, actions: [TextAlertAction], title: String, text: String, placeholder: String, minValue: Int64, value: Int64) { - self.theme = ptheme - self.strings = strings - self.dateTimeFormat = dateTimeFormat - self.title = title - self.text = text - self.placeholder = placeholder - self.minValue = minValue - self.value = value + let inputState = AlertAmountFieldComponent.ExternalState() - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.textNode = ASTextNode() - self.textNode.maximumNumberOfLines = 8 - -// self.inputFieldNode = GiftAuctionCustomBidInputFieldNode(theme: ptheme, placeholder: placeholder) -// self.inputFieldNode.text = "\(value)" - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - -// self.addSubnode(self.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.updateTheme(theme) - } - - deinit { - self.disposable.dispose() - } - - override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) - let spacing: CGFloat = 5.0 - - let titleSize = self.titleNode.measure(measureSize) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) - origin.y += titleSize.height + 4.0 - - let textSize = self.textNode.measure(measureSize) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) - origin.y += textSize.height + 6.0 + spacing - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) - - var contentWidth = max(titleSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let fieldWidth = resultWidth - 18.0 - let amountFieldSize = self.amountField.update( - transition: .immediate, - component: AnyComponent( - AmountFieldComponent( - textColor: self.theme.list.itemPrimaryTextColor, - secondaryColor: self.theme.list.itemSecondaryTextColor, - placeholderColor: self.theme.list.itemPlaceholderTextColor, - accentColor: self.theme.list.itemAccentColor, - value: self.value, - minValue: self.minValue, - forceMinValue: false, - allowZero: false, - maxValue: nil, - placeholderText: self.placeholder, - textFieldOffset: CGPoint(x: -4.0, y: -1.0), - labelText: nil, - currency: .stars, - dateTimeFormat: self.dateTimeFormat, - amountUpdated: { [weak self] value in - guard let self else { - return - } - if let value { - self.value = value - } - }, - tag: nil - ) - ), - environment: {}, - containerSize: CGSize(width: fieldWidth, height: 44.0) + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) ) - var amountFieldFrame = CGRect(origin: CGPoint(x: floor((resultWidth - fieldWidth) / 2.0), y: origin.y - 2.0), size: amountFieldSize) - if let amountFieldView = self.amountField.view { - if amountFieldView.superview == nil { - amountFieldView.clipsToBounds = true - self.backgroundView.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.view.addSubview(self.backgroundView) - self.view.addSubview(amountFieldView) - } - self.backgroundView.frame = amountFieldFrame.insetBy(dx: 7.0, dy: 9.0) - amountFieldFrame.size.width -= 14.0 - amountFieldView.frame = amountFieldFrame - } - - let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + amountFieldSize.height + actionsHeight + insets.top + insets.bottom + 3.0) - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertAmountFieldComponent( + context: context, + initialValue: value, + minValue: minValue, + maxValue: nil, + placeholder: placeholder, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 + ) + ) + )) + + let alertController = AlertScreen( + context: context, + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel, action: { + cancel() + }), + .init(title: action, type: .default, action: { + applyImpl?() + }, autoDismiss: false) + ] + ) + applyImpl = { + if let value = inputState.value, value >= minValue { + apply(value) + } else { + inputState.resetToMinValue() + inputState.animateError() + } + } + return alertController +} + +private final class AlertAmountFieldComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + public class ExternalState { + public fileprivate(set) var value: Int64? + public fileprivate(set) var animateError: () -> Void = {} + public fileprivate(set) var activateInput: () -> Void = {} + public fileprivate(set) var resetToMinValue: () -> Void = {} + fileprivate let valuePromise = ValuePromise(nil) + public var valueSignal: Signal { + return self.valuePromise.get() } - if !hadValidLayout { + public init() { + } + } + + let context: AccountContext + let initialValue: Int64? + let minValue: Int64? + let maxValue: Int64? + let placeholder: String + let isInitiallyFocused: Bool + let externalState: ExternalState + let returnKeyAction: (() -> Void)? + + public init( + context: AccountContext, + initialValue: Int64? = nil, + minValue: Int64? = nil, + maxValue: Int64? = nil, + placeholder: String, + isInitiallyFocused: Bool = false, + externalState: ExternalState, + returnKeyAction: (() -> Void)? = nil + ) { + self.context = context + self.initialValue = initialValue + self.minValue = minValue + self.maxValue = maxValue + self.placeholder = placeholder + self.isInitiallyFocused = isInitiallyFocused + self.externalState = externalState + self.returnKeyAction = returnKeyAction + } + + public static func ==(lhs: AlertAmountFieldComponent, rhs: AlertAmountFieldComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.initialValue != rhs.initialValue { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.isInitiallyFocused != rhs.isInitiallyFocused { + return false + } + return true + } + + public final class View: UIView, UITextFieldDelegate { + private let background = ComponentView() + private let amountField = ComponentView() + + private var currentValue: Int64? + + private var component: AlertAmountFieldComponent? + private weak var state: EmptyComponentState? + + private var isUpdating = false + + func activateInput() { if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { amountFieldView.activateInput() - Queue.mainQueue().justDispatch { - amountFieldView.selectAll() - } } } - return resultSize - } - - func animateError() { - if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { - self.value = self.minValue - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - amountFieldView.resetValue() + func resetToMinValue() { + self.currentValue = self.component?.minValue + self.state?.updated() - amountFieldView.animateError() - amountFieldView.selectAll() + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + amountFieldView.resetValue() + amountFieldView.selectAll() + } + } + + func animateError() { + if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { + amountFieldView.animateError() + } + } + + func update(component: AlertAmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.currentValue = component.initialValue + + component.externalState.animateError = { [weak self] in + self?.animateError() + } + component.externalState.activateInput = { [weak self] in + self?.activateInput() + } + component.externalState.resetToMinValue = { [weak self] in + self?.resetToMinValue() + } + } + + let isFirstTime = self.component == nil + + self.component = component + self.state = state + + let environment = environment[AlertComponentEnvironment.self] + + let topInset: CGFloat = 15.0 + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let amountFieldSize = self.amountField.update( + transition: .immediate, + component: AnyComponent( + AmountFieldComponent( + textColor: environment.theme.actionSheet.primaryTextColor, + secondaryColor: environment.theme.actionSheet.secondaryTextColor, + placeholderColor: environment.theme.actionSheet.inputPlaceholderColor, + accentColor: environment.theme.actionSheet.controlAccentColor, + value: self.currentValue, + minValue: component.minValue, + forceMinValue: false, + allowZero: false, + maxValue: nil, + placeholderText: component.placeholder, + textFieldOffset: CGPoint(x: -4.0, y: -1.0), + labelText: nil, + currency: .stars, + dateTimeFormat: presentationData.dateTimeFormat, + amountUpdated: { [weak self] value in + guard let self else { + return + } + self.currentValue = value + component.externalState.value = value + component.externalState.valuePromise.set(value) + }, + tag: nil + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + var amountFieldFrame = CGRect(origin: CGPoint(x: -16.0, y: topInset - 1.0 + UIScreenPixel), size: amountFieldSize) + if let amountFieldView = self.amountField.view { + if amountFieldView.superview == nil { + amountFieldView.clipsToBounds = true + self.addSubview(amountFieldView) + } + amountFieldFrame.size.width -= 14.0 + amountFieldView.frame = amountFieldFrame + } + + let backgroundPadding: CGFloat = 14.0 + let size = CGSize(width: availableSize.width, height: 50.0) + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent( + FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false) + ), + environment: {}, + containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height) + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + if isFirstTime && component.isInitiallyFocused { + self.activateInput() + } + + return CGSize(width: availableSize.width, height: size.height + topInset) } } - func deactivateInput() { - if let amountFieldView = self.amountField.view as? AmountFieldComponent.View { - amountFieldView.deactivateInput() - } + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - -func giftAuctionCustomBidController(context: AccountContext, title: String, text: String, placeholder: String, action: String, minValue: Int64, value: Int64, apply: @escaping (Int64) -> Void, cancel: @escaping () -> Void) -> AlertController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? - var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - cancel() - }), TextAlertAction(type: .defaultAction, title: action, action: { - applyImpl?() - })] - - let contentNode = GiftAuctionCustomBidAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, actions: actions, title: title, text: text, placeholder: placeholder, minValue: minValue, value: value) - contentNode.complete = { - applyImpl?() - } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - let value = contentNode.value - if value < minValue { - contentNode.animateError() - } else { - dismissImpl?(true) - apply(contentNode.value) - } - } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - }) - controller.dismissed = { byTapOutside in - presentationDataDisposable.dispose() - if byTapOutside { - cancel() - } - } - dismissImpl = { [weak controller, weak contentNode] animated in - contentNode?.deactivateInput() - let _ = contentNode - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index a18bc10a2d..774ce219f0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -3011,9 +3011,7 @@ private final class GiftViewSheetContent: CombinedComponent { originY += 16.0 } else if showUpgradePreview { let title: String - //let description: String let uniqueText: String - //let transferableText: String let tradableText: String if !incoming, case let .profileGift(peerId, _) = subject, let peer = state.peerMap[peerId] { var peerName = peer.compactDisplayTitle @@ -3021,9 +3019,7 @@ private final class GiftViewSheetContent: CombinedComponent { peerName = "\(peerName.prefix(22))…" } title = environment.strings.Gift_Upgrade_GiftTitle - // description = environment.strings.Gift_Upgrade_GiftDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(peerName).string - // transferableText = strings.Gift_Upgrade_Transferable_GiftDescription(peerName).string tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(peerName).string } else if case let .upgradePreview(_, peerName) = component.subject { var peerName = peerName @@ -3031,15 +3027,11 @@ private final class GiftViewSheetContent: CombinedComponent { peerName = "\(peerName.prefix(22))…" } title = environment.strings.Gift_Upgrade_IncludeTitle - // description = environment.strings.Gift_Upgrade_IncludeDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_IncludeDescription - // transferableText = strings.Gift_Upgrade_Transferable_IncludeDescription tradableText = strings.Gift_Upgrade_Tradable_IncludeDescription } else { title = environment.strings.Gift_Upgrade_Title - // description = environment.strings.Gift_Upgrade_Description uniqueText = strings.Gift_Upgrade_Unique_Description - // transferableText = strings.Gift_Upgrade_Transferable_Description tradableText = strings.Gift_Upgrade_Tradable_Description } @@ -3126,7 +3118,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) )), AnyComponentWithIdentity(id: "text", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "View all variants", font: Font.semibold(13.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_ViewAllVariants, font: Font.semibold(13.0), textColor: .white))) )), AnyComponentWithIdentity(id: "arrow", component: AnyComponent( BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white) @@ -3181,20 +3173,6 @@ private final class GiftViewSheetContent: CombinedComponent { )) ) ) -// items.append( -// AnyComponentWithIdentity( -// id: "transferable", -// component: AnyComponent(InfoParagraphComponent( -// title: strings.Gift_Upgrade_Transferable_Title, -// titleColor: textColor, -// text: transferableText, -// textColor: secondaryTextColor, -// accentColor: linkColor, -// iconName: "Premium/Collectible/Transferable", -// iconColor: linkColor -// )) -// ) -// ) items.append( AnyComponentWithIdentity( id: "tradable", @@ -3209,17 +3187,16 @@ private final class GiftViewSheetContent: CombinedComponent { )) ) ) - //TODO:localize items.append( AnyComponentWithIdentity( id: "wearable", component: AnyComponent(InfoParagraphComponent( - title: "Wearable", + title: strings.Gift_Upgrade_Wearable_Title, titleColor: textColor, - text: "Display gifts on your page and set them as profile covers or statuses.", + text: strings.Gift_Upgrade_Wearable_Text, textColor: secondaryTextColor, accentColor: linkColor, - iconName: "Premium/Collectible/Transferable", + iconName: "Premium/Collectible/Wearable", iconColor: linkColor )) ) diff --git a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift index 176bccca06..d1449ab095 100644 --- a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift +++ b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift @@ -504,4 +504,21 @@ public final class GlobalControlPanelsContext { impl.dismissChatListNotice(parentController: parentController, notice: notice) } } + + public var tempVoicePlaylistEnded: (() -> Void)? { + didSet { + let tempVoicePlaylistEnded = self.tempVoicePlaylistEnded + self.impl.with { impl in + impl.tempVoicePlaylistEnded = tempVoicePlaylistEnded + } + } + } + public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? { + didSet { + let tempVoicePlaylistItemChanged = self.tempVoicePlaylistItemChanged + self.impl.with { impl in + impl.tempVoicePlaylistItemChanged = tempVoicePlaylistItemChanged + } + } + } } diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift index 480ec2ea73..8c974a43ef 100644 --- a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/Sources/AccountFreezeInfoScreen.swift @@ -49,7 +49,6 @@ private final class SheetContent: CombinedComponent { final class State: ComponentState { var cachedChevronImage: (UIImage, PresentationTheme)? - var cachedCloseImage: (UIImage, PresentationTheme)? let playOnce = ActionSlot() private var didPlayAnimation = false @@ -189,14 +188,15 @@ private final class SheetContent: CombinedComponent { contentSize.height += list.size.height contentSize.height += spacing + 2.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonAttributedString = NSMutableAttributedString(string: strings.FrozenAccount_SubmitAppeal, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let actionButton = actionButton.update( component: 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: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -209,7 +209,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(actionButton @@ -223,10 +223,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: ButtonComponent( background: ButtonComponent.Background( - color: .clear, - foreground: .clear, - pressedColor: .clear, - cornerRadius: 10.0 + style: .glass, + color: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1), + foreground: theme.list.itemCheckColors.fillColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity( id: AnyHashable(1), @@ -238,19 +238,14 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(closeButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) ) contentSize.height += closeButton.size.height - - if environment.safeInsets.bottom > 0 { - contentSize.height += environment.safeInsets.bottom + 5.0 - } else { - contentSize.height += 12.0 - } + contentSize.height += buttonInsets.bottom state.playAnimationIfNeeded() @@ -312,6 +307,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, @@ -507,8 +503,8 @@ private final class ParagraphComponent: CombinedComponent { textColor: component.titleColor, paragraphAlignment: .natural )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 + horizontalAlignment: .natural, + maximumNumberOfLines: 2 ), availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), transition: .immediate @@ -576,24 +572,3 @@ private final class ParagraphComponent: CombinedComponent { } } } - -private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD index 174ee14b59..dd67a3b410 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD @@ -20,6 +20,8 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift index b5bd40fb9d..8a531ac3a3 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift @@ -10,533 +10,73 @@ import AccountContext import ComponentFlow import MultilineTextComponent import BalancedTextComponent +import AlertComponent +import AlertInputFieldComponent -private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: EditableTextNode - private let placeholderNode: ASTextNode - private let characterLimitView = ComponentView() - - private let characterLimit: Int - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets: UIEdgeInsets - - private let validCharacterSets: [CharacterSet] - - var text: String { - get { - return self.textInputNode.attributedText?.string ?? "" - } - set { - self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor) - self.placeholderNode.isHidden = !newValue.isEmpty - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String, characterLimit: Int) { - self.theme = theme - self.characterLimit = characterLimit - - self.inputInsets = UIEdgeInsets(top: 9.0, left: 6.0, bottom: 9.0, right: 16.0) - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = EditableTextNode() - self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(13.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] - self.textInputNode.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: self.inputInsets.left, bottom: self.inputInsets.bottom, right: self.inputInsets.right) - self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.keyboardType = .default - self.textInputNode.autocapitalizationType = .none - self.textInputNode.returnKeyType = .done - self.textInputNode.autocorrectionType = .no - self.textInputNode.tintColor = theme.actionSheet.controlAccentColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - self.validCharacterSets = [ - CharacterSet.alphanumerics, - CharacterSet(charactersIn: "0123456789_ "), - ] - - super.init() - - self.textInputNode.delegate = self - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - let characterLimitString: String - let characterLimitColor: UIColor - if self.text.count <= self.characterLimit { - let remaining = self.characterLimit - self.text.count - if remaining < 5 { - characterLimitString = "\(remaining)" - } else { - characterLimitString = " " - } - characterLimitColor = self.theme.list.itemPlaceholderTextColor - } else { - characterLimitString = "\(self.characterLimit - self.text.count)" - characterLimitColor = self.theme.list.itemDestructiveColor - } - - let characterLimitSize = self.characterLimitView.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: characterLimitString, font: Font.regular(13.0), textColor: characterLimitColor)) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) +public func businessLinkNameAlertController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + value: String?, + apply: @escaping (String?) -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let inputState = AlertInputFieldComponent.ExternalState() + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Business_Links_LinkNameTitle) ) - if let characterLimitComponentView = self.characterLimitView.view { - if characterLimitComponentView.superview == nil { - self.view.addSubview(characterLimitComponentView) - } - characterLimitComponentView.frame = CGRect(origin: CGPoint(x: width - 23.0 - characterLimitSize.width, y: 18.0), size: characterLimitSize) - } - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - self.updateTextNodeText(animated: true) - self.textChanged?(editableTextNode.textView.text) - self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty - } - - func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if text == "\n" { - self.complete?() - return false - } - if text.unicodeScalars.contains(where: { c in - return !self.validCharacterSets.contains(where: { set in - return set.contains(c) - }) - }) { - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(34.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(34.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.attributedText = nil - self.deactivateInput() - } -} - -public final class BusinessLinkNameAlertContentNode: AlertContentNode { - private let context: AccountContext - private var theme: AlertControllerTheme - private let strings: PresentationStrings - private let text: String - private let subtext: String - private let titleFont: PromptControllerTitleFont - - private let textView = ComponentView() - private let subtextView = ComponentView() - - fileprivate let inputFieldNode: PromptInputFieldNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let disposable = MetaDisposable() - - private var validLayout: CGSize? - private var errorText: String? - - private let hapticFeedback = HapticFeedback() - - var complete: (() -> Void)? - - override public var dismissOnOutsideTap: Bool { - return self.isUserInteractionEnabled - } - - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, subtext: String, titleFont: PromptControllerTitleFont, value: String?, characterLimit: Int) { - self.context = context - self.theme = theme - self.strings = strings - self.text = text - self.subtext = subtext - self.titleFont = titleFont - - self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: strings.Business_Links_LinkNameInputPlaceholder, characterLimit: characterLimit) - self.inputFieldNode.text = value ?? "" - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - - self.actionNodes = actions.map { action -> TextAlertContentActionNode in - return TextAlertContentActionNode(theme: theme, action: action) - } - - var actionVerticalSeparators: [ASDisplayNode] = [] - if actions.count > 1 { - for _ in 0 ..< actions.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.inputFieldNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - self.actionNodes.last?.actionEnabled = true - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.inputFieldNode.updateHeight = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.validLayout { - strongSelf.requestLayout?(.immediate) - } - } - } - - self.inputFieldNode.textChanged = { [weak self] text in - if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { - lastNode.actionEnabled = text.count <= characterLimit - strongSelf.requestLayout?(.immediate) - } - } - - self.updateTheme(theme) - - self.inputFieldNode.complete = { [weak self] in - guard let self else { - return - } - if let lastNode = self.actionNodes.last, lastNode.actionEnabled { - self.complete?() - } - } - } - - deinit { - self.disposable.dispose() - } - - var value: String { - return self.inputFieldNode.text - } - - public func setErrorText(errorText: String?) { - if self.errorText != errorText { - self.errorText = errorText - self.requestLayout?(.immediate) - } - - if errorText != nil { - HapticFeedback().error() - self.inputFieldNode.layer.addShakeAnimation() - } - } - - override public func updateTheme(_ theme: AlertControllerTheme) { - self.theme = theme - - self.actionNodesSeparator.backgroundColor = theme.separatorColor - for actionNode in self.actionNodes { - actionNode.updateTheme(theme) - } - for separatorNode in self.actionVerticalSeparators { - separatorNode.backgroundColor = theme.separatorColor - } - - if let size = self.validLayout { - _ = self.updateLayout(size: size, transition: .immediate) - } - } - - override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - var size = size - size.width = min(size.width, 270.0) - let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) - - let hadValidLayout = self.validLayout != nil - - self.validLayout = size - - var origin: CGPoint = CGPoint(x: 0.0, y: 16.0) - let spacing: CGFloat = 5.0 - let subtextSpacing: CGFloat = -1.0 - - let textSize = self.textView.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: measureSize.width, height: 1000.0) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Business_Links_LinkNameText)) ) - let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: origin.y), size: textSize) - if let textComponentView = self.textView.view { - if textComponentView.superview == nil { - textComponentView.layer.anchorPoint = CGPoint() - self.view.addSubview(textComponentView) - } - textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) - transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) - } - origin.y += textSize.height + 6.0 + subtextSpacing - - let subtextSize = self.subtextView.update( - transition: .immediate, - component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: self.errorText ?? self.subtext, font: Font.regular(13.0), textColor: self.errorText != nil ? self.theme.destructiveColor : self.theme.primaryColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: measureSize.width, height: 1000.0) - ) - let subtextFrame = CGRect(origin: CGPoint(x: floor((size.width - subtextSize.width) * 0.5), y: origin.y), size: subtextSize) - if let subtextComponentView = self.subtextView.view { - if subtextComponentView.superview == nil { - subtextComponentView.layer.anchorPoint = CGPoint() - self.view.addSubview(subtextComponentView) - } - subtextComponentView.bounds = CGRect(origin: CGPoint(), size: subtextFrame.size) - transition.updatePosition(layer: subtextComponentView.layer, position: subtextFrame.origin) - } - origin.y += subtextSize.height + 6.0 + spacing - - let actionButtonHeight: CGFloat = 44.0 - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - - var effectiveActionLayout = TextAlertContentActionLayout.horizontal - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { - effectiveActionLayout = .vertical - } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } - } - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) - - var contentWidth = max(textSize.width, minActionsWidth) - contentWidth = max(subtextSize.width, minActionsWidth) - contentWidth = max(contentWidth, 234.0) - - var actionsHeight: CGFloat = 0.0 - switch effectiveActionLayout { - case .horizontal: - actionsHeight = actionButtonHeight - case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) - } - - let resultWidth = contentWidth + insets.left + insets.right - - let inputFieldWidth = resultWidth - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - let inputHeight = inputFieldHeight - let inputFieldFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) - transition.updateFrame(node: self.inputFieldNode, frame: inputFieldFrame) - transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) - - let resultSize = CGSize(width: resultWidth, height: textSize.height + subtextSpacing + subtextSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) - - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - switch effectiveActionLayout { - case .horizontal: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - case .vertical: - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - switch effectiveActionLayout { - case .horizontal: - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - case .vertical: - currentActionWidth = resultSize.width - } - - let actionNodeFrame: CGRect - switch effectiveActionLayout { - case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += currentActionWidth - case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } - - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if !hadValidLayout { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - func animateError() { - self.inputFieldNode.layer.addShakeAnimation() - self.hapticFeedback.error() - } -} + )) -public enum PromptControllerTitleFont { - case regular - case bold -} - -public func businessLinkNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - var dismissImpl: ((Bool) -> Void)? var applyImpl: (() -> Void)? - - let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - apply(nil) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - applyImpl?() - })] - - let contentNode = BusinessLinkNameAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, subtext: subtext, titleFont: titleFont, value: value, characterLimit: characterLimit) - contentNode.complete = { - applyImpl?() + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + initialValue: value, + placeholder: strings.Business_Links_LinkNameInputPlaceholder, + characterLimit: 32, + hasClearButton: false, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var effectiveUpdatedPresentationData: (PresentationData, Signal) + if let updatedPresentationData { + effectiveUpdatedPresentationData = updatedPresentationData + } else { + effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - applyImpl = { [weak contentNode] in - guard let contentNode = contentNode else { - return - } - apply(contentNode.value) + + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Common_Done, type: .default, action: { + applyImpl?() + }) + ], + updatedPresentationData: effectiveUpdatedPresentationData + ) + applyImpl = { + apply(inputState.value) } - - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) - let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in - controller?.theme = AlertControllerTheme(presentationData: presentationData) - contentNode?.inputFieldNode.updateTheme(presentationData.theme) - }) - controller.dismissed = { _ in - presentationDataDisposable.dispose() - } - dismissImpl = { [weak controller] animated in - contentNode.inputFieldNode.deactivateInput() - if animated { - controller?.dismissAnimated() - } else { - controller?.dismiss() - } - } - return controller + return alertController } diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD index a9f96d6606..99a8b1bbc7 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD @@ -21,6 +21,8 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BalancedTextComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift index 9629f3f3bb..7966d579a2 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -11,6 +11,8 @@ import ComponentFlow import MultilineTextComponent import BalancedTextComponent import EmojiStatusComponent +import AlertComponent +import AlertInputFieldComponent private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private var theme: PresentationTheme @@ -500,6 +502,100 @@ public enum PromptControllerTitleFont { } public func quickReplyNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController { +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// let strings = presentationData.strings +// +// let inputState = AlertInputFieldComponent.ExternalState() +// +// let doneIsEnabled: Signal = inputState.valueSignal +// |> map { value in +// return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty +// } +// +// let doneInProgressValuePromise = ValuePromise(false) +// let doneInProgress = doneInProgressValuePromise.get() +// +// var content: [AnyComponentWithIdentity] = [] +// content.append(AnyComponentWithIdentity( +// id: "title", +// component: AnyComponent( +// AlertTitleComponent(title: strings.WebBrowser_Exceptions_Create_Title) +// ) +// )) +// content.append(AnyComponentWithIdentity( +// id: "text", +// component: AnyComponent( +// AlertTextComponent(content: .plain(strings.WebBrowser_Exceptions_Create_Text)) +// ) +// )) +// +// let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) +// let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) +// var applyImpl: (() -> Void)? +// content.append(AnyComponentWithIdentity( +// id: "input", +// component: AnyComponent( +// AlertInputFieldComponent( +// context: context, +// initialValue: nil, +// placeholder: strings.QuickReply_ShortcutPlaceholder, +// characterLimit: characterLimit, +// hasClearButton: false, +// keyboardType: .URL, +// autocapitalizationType: .none, +// autocorrectionType: .no, +// isInitiallyFocused: true, +// externalState: inputState, +// shouldChangeText: { updatedText in +// guard let domainRegex, let pathRegex else { +// return true +// } +// let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) +// let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) +// if domainMatches.count > 0, pathMatches.count == 0 { +// return true +// } else { +// return false +// } +// }, +// returnKeyAction: { +// applyImpl?() +// } +// ) +// ) +// )) +// +// var effectiveUpdatedPresentationData: (PresentationData, Signal) +// if let updatedPresentationData { +// effectiveUpdatedPresentationData = updatedPresentationData +// } else { +// effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) +// } +// +// let alertController = AlertScreen( +// configuration: AlertScreen.Configuration(allowInputInset: true), +// content: content, +// actions: [ +// .init(title: strings.Common_Cancel, action: { +// apply(nil) +// }), +// .init(title: strings.Common_Done, type: .default, action: { +// applyImpl?() +// }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgress) +// ], +// updatedPresentationData: effectiveUpdatedPresentationData +// ) +// applyImpl = { +// let updatedLink = explicitUrl(inputState.value) +// if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { +// doneInProgressValuePromise.set(true) +// apply(updatedLink) +// } else { +// inputState.animateError() +// } +// } +// return alertController + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var dismissImpl: ((Bool) -> Void)? diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index eff457d8f6..7677a5af7e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -1646,7 +1646,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { style: .glass, color: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1), foreground: theme.list.itemCheckColors.fillColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8), + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity( id: AnyHashable(0), diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json new file mode 100644 index 0000000000..f3819ec936 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cocoon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/cocoon.png b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/cocoon.png new file mode 100644 index 0000000000..0d5c7455dc Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Cocoon.imageset/cocoon.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearable.imageset/Contents.json similarity index 100% rename from submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearable.imageset/Contents.json diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearable.imageset/wear_30 (2).pdf similarity index 100% rename from submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearing.imageset/wear_30 (2).pdf rename to submodules/TelegramUI/Images.xcassets/Premium/Collectible/Wearable.imageset/wear_30 (2).pdf diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index bd8f736a67..fc4551824f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -4647,6 +4647,18 @@ extension ChatControllerImpl { groupCalls: groupCallPanelSource, chatListNotices: false ) + globalControlPanelsContext.tempVoicePlaylistEnded = { [weak self] in + guard let self else { + return + } + self.tempVoicePlaylistEnded?() + } + globalControlPanelsContext.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in + guard let self else { + return + } + self.tempVoicePlaylistItemChanged?(previousItem, currentItem) + } self.globalControlPanelsContext = globalControlPanelsContext self.globalControlPanelsContextStateDisposable = (globalControlPanelsContext.state |> deliverOnMainQueue).startStrict(next: { [weak self] state in diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index 85b0428962..4a21f446b8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -75,22 +75,19 @@ extension ChatControllerImpl { var completion: ((String?) -> Void)? let alertController = businessLinkNameAlertController( context: self.context, - text: self.presentationData.strings.Business_Links_LinkNameTitle, - subtext: self.presentationData.strings.Business_Links_LinkNameText, value: currentValue, - characterLimit: 32, apply: { value in completion?(value) } ) completion = { [weak self, weak alertController] value in guard let self else { - alertController?.dismissAnimated() + alertController?.dismiss(completion: nil) return } if let value { if value == currentValue { - alertController?.dismissAnimated() + alertController?.dismiss(completion: nil) return } @@ -116,7 +113,7 @@ extension ChatControllerImpl { } alertController?.view.endEditing(true) - alertController?.dismissAnimated() + alertController?.dismiss(completion: nil) } } self.present(alertController, in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e6d81edfdc..b6de849147 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -93,6 +93,7 @@ import NewContactScreen import PasskeysScreen import GiftDemoScreen import ChatTextLinkEditUI +import CocoonInfoScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -4024,6 +4025,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return SendInviteLinkScreen(context: context, subject: subject, peers: peers, theme: theme) } + public func makeCocoonInfoScreen(context: AccountContext) -> ViewController { + return CocoonInfoScreen(context: context) + } + public func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController { return chatTextLinkEditController(context: context, updatedPresentationData: updatedPresentationData, text: text, link: link, apply: apply) }