diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index bd7e0b4e99..ff8fe0d47a 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -211,7 +211,7 @@ public final class ResizableSheetComponent Void) { @@ -518,20 +515,14 @@ public final class ResizableSheetComponent NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.copyProtectionTips.key) } + + static func aiTextProcessingStyleSelectionTips() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.aiTextProcessingStyleSelectionTips.key) + } } public struct ApplicationSpecificNotice { @@ -2578,4 +2583,31 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getAITextProcessingStyleSelection(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.aiTextProcessingStyleSelectionTips())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementAITextProcessingStyleSelection(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.aiTextProcessingStyleSelectionTips())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.aiTextProcessingStyleSelectionTips(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index 2091e7d0ce..9ab506f846 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -58,6 +58,14 @@ private final class RestingBackgroundView: UIVisualEffectView { } public final class LiquidLensView: UIView { + public final class TransitionInfo { + public let disableAnimationWorkarounds: Bool + + public init(disableAnimationWorkarounds: Bool) { + self.disableAnimationWorkarounds = disableAnimationWorkarounds + } + } + public enum Kind { case externalContainer case builtinContainer @@ -374,8 +382,11 @@ public final class LiquidLensView: UIView { lensView.bounds = lensBounds } - lensView.layer.removeAllAnimations() - lensView.bounds = lensBounds + if let info = transition.userData(TransitionInfo.self), info.disableAnimationWorkarounds { + } else { + lensView.layer.removeAllAnimations() + lensView.bounds = lensBounds + } if !transition.animation.isImmediate { self.isAnimating = true diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index d9d04e163a..0ebf550a13 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -828,7 +828,7 @@ public final class TabBarComponent: Component { itemTransition.setFrame(view: itemComponentView, frame: itemFrame) itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center) itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) - itemTransition.setScale(view: selectedItemComponentView, scale: self.selectionGestureState != nil ? 1.15 : 1.0) + itemTransition.setScale(view: selectedItemComponentView, scale: (self.selectionGestureState != nil && component.isLiftedStateEnabled) ? 1.15 : 1.0) if let previousComponent, previousComponent.selectedId != item.id, isItemSelected { itemComponentView.playSelectionAnimation() @@ -880,7 +880,7 @@ public final class TabBarComponent: Component { lensSelection.x = max(0.0, min(lensSelection.x, lensSize.width - lensSelection.width)) - self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil && component.isLiftedStateEnabled, isCollapsed: isLensCollapsed, transition: transition) + self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil && component.isLiftedStateEnabled, isCollapsed: isLensCollapsed, transition: transition.withUserData(LiquidLensView.TransitionInfo(disableAnimationWorkarounds: !component.isLiftedStateEnabled))) var size = tabsSize diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD index 497712af64..aabef09139 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD +++ b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD @@ -35,6 +35,10 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/TooltipComponent", + "//submodules/TelegramUI/Components/ToastComponent", + "//submodules/TelegramNotices", + "//submodules/Markdown", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift index 46354ec2b5..4649bdfa59 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift @@ -536,8 +536,15 @@ final class TextProcessingLanguageSelectionComponent: Component { } let stylesContentSize = stylesSize if var stylesSizeValue = stylesSize { + let stylesItemHeight: CGFloat = 60.0 let maxHeight = min(370.0, maxAvailableHeight) - stylesSizeValue.height = min(stylesSizeValue.height, maxHeight) + if stylesSizeValue.height > maxHeight { + let n = floor(maxHeight / stylesItemHeight) + let visibleHeight = (n - 0.5) * stylesItemHeight + stylesSizeValue.height = max(stylesItemHeight, visibleHeight) + } else { + stylesSizeValue.height = min(stylesSizeValue.height, maxHeight) + } stylesSize = stylesSizeValue mainSize.height = min(mainSize.height, stylesSizeValue.height) } diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift index c0df88b645..ef7305d50d 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift @@ -19,6 +19,9 @@ import TranslateUI import LottieComponent import ListSectionComponent import ListActionItemComponent +import ToastComponent +import TelegramNotices +import Markdown final class TextProcessingContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -37,6 +40,7 @@ final class TextProcessingContentComponent: Component { let mode: TextProcessingScreen.Mode let styles: [TelegramComposeAIMessageMode.Style] let inputText: TextWithEntities + let shouldDisplayStyleNotice: Bool let copyCurrentResult: (() -> Void)? let translateChat: ((String) -> Void)? let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void @@ -47,6 +51,7 @@ final class TextProcessingContentComponent: Component { mode: TextProcessingScreen.Mode, styles: [TelegramComposeAIMessageMode.Style], inputText: TextWithEntities, + shouldDisplayStyleNotice: Bool, copyCurrentResult: (() -> Void)?, translateChat: ((String) -> Void)?, displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void @@ -56,6 +61,7 @@ final class TextProcessingContentComponent: Component { self.context = context self.mode = mode self.inputText = inputText + self.shouldDisplayStyleNotice = shouldDisplayStyleNotice self.copyCurrentResult = copyCurrentResult self.translateChat = translateChat self.displayLanguageSelectionMenu = displayLanguageSelectionMenu @@ -100,71 +106,33 @@ final class TextProcessingContentComponent: Component { self.addSubview(self.currentContentBackground) self.addSubview(self.currentContentContainer) - self.translateState.resultUpdated = { [weak self] result in - guard let self, let component = self.component else { - return - } - if case .translate = self.currentMode { - component.externalState.result = result?.text - } + + self.translateState.resultUpdated = { [weak self] _ in + self?.externalStatesUpdated() } - self.translateState.isProcessingUpdated = { [weak self] isProcessing in - guard let self, let component = self.component else { - return - } - if case .translate = self.currentMode { - component.externalState.isProcessing = isProcessing - } + self.translateState.isProcessingUpdated = { [weak self] _ in + self?.externalStatesUpdated() } self.translateState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in - guard let self else { - return - } - self.nonPremiumFloodTriggeredUpdated() + self?.externalStatesUpdated() } - self.stylizeState.resultUpdated = { [weak self] result in - guard let self, let component = self.component else { - return - } - if case .stylize = self.currentMode { - component.externalState.result = result?.text - } + self.stylizeState.resultUpdated = { [weak self] _ in + self?.externalStatesUpdated() } - self.stylizeState.isProcessingUpdated = { [weak self] isProcessing in - guard let self, let component = self.component else { - return - } - if case .stylize = self.currentMode { - component.externalState.isProcessing = isProcessing - } + self.stylizeState.isProcessingUpdated = { [weak self] _ in + self?.externalStatesUpdated() } self.stylizeState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in - guard let self else { - return - } - self.nonPremiumFloodTriggeredUpdated() + self?.externalStatesUpdated() } - self.fixState.resultUpdated = { [weak self] result in - guard let self, let component = self.component else { - return - } - if case .fix = self.currentMode { - component.externalState.result = result?.text - } + self.fixState.resultUpdated = { [weak self] _ in + self?.externalStatesUpdated() } - self.fixState.isProcessingUpdated = { [weak self] isProcessing in - guard let self, let component = self.component else { - return - } - if case .fix = self.currentMode { - component.externalState.isProcessing = isProcessing - } + self.fixState.isProcessingUpdated = { [weak self] _ in + self?.externalStatesUpdated() } self.fixState.nonPremiumFloodTriggeredUpdated = { [weak self] _ in - guard let self else { - return - } - self.nonPremiumFloodTriggeredUpdated() + self?.externalStatesUpdated() } } @@ -172,11 +140,28 @@ final class TextProcessingContentComponent: Component { preconditionFailure() } - private func nonPremiumFloodTriggeredUpdated() { + private func externalStatesUpdated() { guard let component = self.component else { return } + + switch self.currentMode { + case .translate: + component.externalState.isProcessing = self.translateState.isProcessing + component.externalState.result = self.translateState.result?.text + case .stylize: + component.externalState.isProcessing = self.stylizeState.isProcessing + component.externalState.result = self.stylizeState.result?.text + case .fix: + component.externalState.isProcessing = self.fixState.isProcessing + component.externalState.result = self.fixState.result?.text + } + component.externalState.nonPremiumFloodTriggered = self.translateState.nonPremiumFloodTriggered || self.stylizeState.nonPremiumFloodTriggered || self.fixState.nonPremiumFloodTriggered + + /*#if DEBUG + component.externalState.nonPremiumFloodTriggered = true + #endif*/ } func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -189,6 +174,10 @@ final class TextProcessingContentComponent: Component { let environment = environment[ViewControllerComponentContainer.Environment.self].value + if self.component == nil { + self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice + } + self.component = component self.state = state @@ -213,6 +202,7 @@ final class TextProcessingContentComponent: Component { } if self.currentMode != .translate { self.currentMode = .translate + self.externalStatesUpdated() } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) @@ -228,11 +218,13 @@ final class TextProcessingContentComponent: Component { icon: .bundleIcon(name: "TextProcessing/TabStylize") )), action: { [weak self] _ in - guard let self else { + guard let self, let component = self.component else { return } if self.currentMode != .stylize { self.currentMode = .stylize + let _ = ApplicationSpecificNotice.incrementAITextProcessingStyleSelection(accountManager: component.context.sharedContext.accountManager).startStandalone() + self.externalStatesUpdated() } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) @@ -253,6 +245,7 @@ final class TextProcessingContentComponent: Component { } if self.currentMode != .fix { self.currentMode = .fix + self.externalStatesUpdated() } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) @@ -505,6 +498,7 @@ private final class TextProcessingSheetComponent: Component { let ignoredTranslationLanguages: [String] let styles: [TelegramComposeAIMessageMode.Style] let inputText: TextWithEntities + let shouldDisplayStyleNotice: Bool let copyCurrentResult: ((TextWithEntities) -> Void)? let translateChat: ((String) -> Void)? @@ -514,6 +508,7 @@ private final class TextProcessingSheetComponent: Component { ignoredTranslationLanguages: [String], styles: [TelegramComposeAIMessageMode.Style], inputText: TextWithEntities, + shouldDisplayStyleNotice: Bool, copyCurrentResult: ((TextWithEntities) -> Void)?, translateChat: ((String) -> Void)? ) { @@ -522,6 +517,7 @@ private final class TextProcessingSheetComponent: Component { self.ignoredTranslationLanguages = ignoredTranslationLanguages self.styles = styles self.inputText = inputText + self.shouldDisplayStyleNotice = shouldDisplayStyleNotice self.copyCurrentResult = copyCurrentResult self.translateChat = translateChat } @@ -532,6 +528,7 @@ private final class TextProcessingSheetComponent: Component { final class View: UIView { private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private var toast: ComponentView? private var languageSelectionMenu: ComponentView? private let animateOut = ActionSlot>() private let contentExternalState = TextProcessingContentComponent.ExternalState() @@ -593,10 +590,12 @@ private final class TextProcessingSheetComponent: Component { let isMainActionEnabled: Bool //TODO:localize (for AI: don't remove this line) let actionButtonTitle: String + var actionButtonShowsIncreaseLimit = false if self.contentExternalState.nonPremiumFloodTriggered { isMainActionEnabled = true actionButtonTitle = "Increase Limit" + actionButtonShowsIncreaseLimit = true performMainAction = { [weak self] in guard let self, let component = self.component else { return @@ -664,6 +663,7 @@ private final class TextProcessingSheetComponent: Component { mode: component.mode, styles: component.styles, inputText: component.inputText, + shouldDisplayStyleNotice: component.shouldDisplayStyleNotice, copyCurrentResult: component.copyCurrentResult != nil ? { copyCurrentResultImpl() } : nil, @@ -725,6 +725,7 @@ private final class TextProcessingSheetComponent: Component { ActionButtonsComponent( theme: theme, actionTitle: actionButtonTitle, + actionButtonShowsIncreaseLimit: actionButtonShowsIncreaseLimit, action: isMainActionEnabled ? performMainAction : nil, sendAction: performSendAction.flatMap { [weak self] performSendAction in return { @@ -738,29 +739,6 @@ private final class TextProcessingSheetComponent: 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(ButtonTextContentComponent( - text: actionButtonTitle, - badge: 0, - textColor: theme.list.itemCheckColors.foregroundColor, - badgeBackground: theme.list.itemCheckColors.foregroundColor, - badgeForeground: theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: isMainActionEnabled, - displaysProgress: false, - action: { - performMainAction() - } - )*/ ), backgroundColor: .color(theme.list.blocksBackgroundColor), animateOut: self.animateOut @@ -792,6 +770,69 @@ private final class TextProcessingSheetComponent: Component { } transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) } + + if self.contentExternalState.nonPremiumFloodTriggered { + let sideInset: CGFloat = 8.0 + + let toast: ComponentView + var toastTransition = transition + if let current = self.toast { + toast = current + } else { + toastTransition = toastTransition.withAnimation(.none) + toast = ComponentView() + self.toast = toast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let playOnce = ActionSlot() + let toastSize = toast.update( + transition: toastTransition, + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_infotip"), + startingPosition: .begin, + size: CGSize(width: 32.0, height: 32.0), + playOnce: playOnce + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Daily limit reached", font: Font.semibold(14.0), textColor: .white)), + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: "Get **Telegram Premium** for **50x** more text edits per day.", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let toastView = toast.view { + if toastView.superview == nil, let sheetView = self.sheet.view as? ResizableSheetComponent.View { + sheetView.containerView.addSubview(toastView) + if !transition.animation.isImmediate { + toastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + if let toastView = toastView as? ToastContentComponent.View, let iconView = toastView.iconView as? LottieComponent.View { + iconView.playOnce() + } + } + toastTransition.setFrame(view: toastView, frame: CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - 94.0 - toastSize.height), size: toastSize)) + } + } else { + if let toast = self.toast { + self.toast = nil + if let toastView = toast.view { + toastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak toastView] _ in + toastView?.removeFromSuperview() + }) + } + } + } if let languageSelectionMenuDataValue = self.languageSelectionMenuData { let languageSelectionMenu: ComponentView @@ -867,6 +908,8 @@ public class TextProcessingScreen: ViewControllerComponentContainer { self.context = context let styles = await context.engine.messages.composeAIMessageStyles().get() + + let shouldDisplayStyleNotice = await ApplicationSpecificNotice.getAITextProcessingStyleSelection(accountManager: context.sharedContext.accountManager).get() < 3 super.init( context: context, @@ -876,6 +919,7 @@ public class TextProcessingScreen: ViewControllerComponentContainer { ignoredTranslationLanguages: ignoredTranslationLanguages, styles: styles, inputText: inputText, + shouldDisplayStyleNotice: shouldDisplayStyleNotice, copyCurrentResult: copyResult, translateChat: translateChat ), @@ -1028,17 +1072,20 @@ private final class TitleComponent: Component { private final class ActionButtonsComponent: Component { let theme: PresentationTheme let actionTitle: String + let actionButtonShowsIncreaseLimit: Bool let action: (() -> Void)? let sendAction: (() -> Void)? init( theme: PresentationTheme, actionTitle: String, + actionButtonShowsIncreaseLimit: Bool, action: (() -> Void)?, sendAction: (() -> Void)? ) { self.theme = theme self.actionTitle = actionTitle + self.actionButtonShowsIncreaseLimit = actionButtonShowsIncreaseLimit self.action = action self.sendAction = sendAction } @@ -1050,6 +1097,9 @@ private final class ActionButtonsComponent: Component { if lhs.actionTitle != rhs.actionTitle { return false } + if lhs.actionButtonShowsIncreaseLimit != rhs.actionButtonShowsIncreaseLimit { + return false + } if (lhs.action == nil) != (rhs.action == nil) { return false } @@ -1084,6 +1134,17 @@ private final class ActionButtonsComponent: Component { actionButtonWidth -= 52.0 + spacing } + var actionButtonContents: [AnyComponentWithIdentity] = [] + actionButtonContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.actionTitle, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )))) + if component.actionButtonShowsIncreaseLimit { + actionButtonContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(IncreaseLimitBadgeComponent( + fillColor: component.theme.list.itemCheckColors.foregroundColor, + foregroundColor: .clear + )))) + } + let actionButtonSize = self.actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -1095,12 +1156,9 @@ private final class ActionButtonsComponent: Component { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(ButtonTextContentComponent( - text: component.actionTitle, - badge: 0, - textColor: component.theme.list.itemCheckColors.foregroundColor, - badgeBackground: component.theme.list.itemCheckColors.foregroundColor, - badgeForeground: component.theme.list.itemCheckColors.fillColor + component: AnyComponent(HStack( + actionButtonContents, + spacing: 6.0 )) ), isEnabled: component.action != nil, @@ -1175,3 +1233,86 @@ private final class ActionButtonsComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private final class IncreaseLimitBadgeComponent: Component { + let fillColor: UIColor + let foregroundColor: UIColor + + init( + fillColor: UIColor, + foregroundColor: UIColor + ) { + self.fillColor = fillColor + self.foregroundColor = foregroundColor + } + + static func ==(lhs: IncreaseLimitBadgeComponent, rhs: IncreaseLimitBadgeComponent) -> Bool { + if lhs.fillColor != rhs.fillColor { + return false + } + if lhs.foregroundColor != rhs.foregroundColor { + return false + } + return true + } + + final class View: UIView { + private let iconView: UIImageView + + private var component: IncreaseLimitBadgeComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.iconView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: IncreaseLimitBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let leftInset: CGFloat = 4.0 + let rightInset: CGFloat = 3.0 + let topInset: CGFloat = 1.0 + let bottomInset: CGFloat = 0.0 + + let text = NSAttributedString(string: "X50", font: Font.with(size: 14.0, design: .round, weight: .semibold), textColor: .clear) + let rawTextSize = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + let textSize = CGSize(width: ceil(rawTextSize.width), height: ceil(rawTextSize.height)) + let backgroundSize = CGSize(width: leftInset + rightInset + textSize.width, height: topInset + bottomInset + textSize.height) + + self.iconView.image = generateImage(backgroundSize, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(component.fillColor.cgColor) + UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 4.0).fill() + + if component.foregroundColor.alpha != 1.0 { + context.setBlendMode(.copy) + } + text.draw(at: CGPoint(x: leftInset, y: topInset)) + }) + self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize) + + return backgroundSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift index 89243cf666..6b14d2a059 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift @@ -137,8 +137,23 @@ final class TextProcessingStyleSelectionComponent: Component { styleData.append((item.id, item.emoji, localizedStyleName(strings: component.strings, styleId: item.id))) } - let minSlotWidth: CGFloat = max(50.0, floor(availableSize.width / 5.0)) - let slotWidth = max(minSlotWidth, floor(availableSize.width / CGFloat(styleData.count))) + let minSlotWidth: CGFloat = 50.0 + let slotWidth: CGFloat + if CGFloat(styleData.count) * minSlotWidth <= availableSize.width { + slotWidth = floor(availableSize.width / CGFloat(styleData.count)) + } else { + var resolved: CGFloat = minSlotWidth + var targetVisible: CGFloat = min(7.5, floor((availableSize.width + 16.0) / 60.0) + 0.5) + while targetVisible >= 1.5 { + let candidateWidth = floor((availableSize.width + 16.0) / targetVisible) + if candidateWidth >= minSlotWidth { + resolved = candidateWidth + break + } + targetVisible -= 1.0 + } + slotWidth = resolved + } let contentWidth = slotWidth * CGFloat(styleData.count) let itemSize = CGSize(width: slotWidth, height: availableSize.height) diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift index 9216738ae5..ac3224fa07 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTextAreaComponent.swift @@ -417,13 +417,18 @@ final class TextProcessingTextAreaComponent: Component { action: { isExpanded.toggle() }, - animateAlpha: false, + animateAlpha: true, animateScale: false, animateContents: false )), environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) ) + + if isExpanded.value && textLayout.trailingLineWidth >= availableSize.width - sideInset - expandButtonSize.width - 8.0 { + textContainerFrame.size.height += 22.0 + } + let expandButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - expandButtonSize.width, y: textContainerFrame.maxY - expandButtonSize.height - 2.0), size: expandButtonSize) if let expandButtonView = expandButton.view { if expandButtonView.superview == nil { @@ -434,7 +439,9 @@ final class TextProcessingTextAreaComponent: Component { expandButtonTransition.setBounds(view: expandButtonView, bounds: CGRect(origin: CGPoint(), size: expandButtonFrame.size)) expandButtonTransition.setFrame(view: expandShadow, frame: expandButtonFrame.insetBy(dx: -expandShadowExtent, dy: -expandShadowExtent)) + expandButtonTransition.setAlpha(view: expandShadow, alpha: isExpanded.value ? 0.0 : 1.0) } + } else { if let expandButton = self.expandButton { self.expandButton = nil @@ -444,6 +451,12 @@ final class TextProcessingTextAreaComponent: Component { self.expandShadow = nil expandShadow.removeFromSuperview() } + + if component.copyAction != nil, let textLayout = self.textState.layout { + if textLayout.trailingLineWidth >= availableSize.width - sideInset - 32.0 { + textContainerFrame.size.height += 28.0 + } + } } if component.text == nil { diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift index a4a3a1c981..df326e7ade 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift @@ -10,6 +10,7 @@ import MultilineTextComponent import BundleIconComponent import TelegramCore import TranslateUI +import TooltipComponent private let languageRecognizer = NLLanguageRecognizer() @@ -49,6 +50,7 @@ final class TextProcessingTranslateContentComponent: Component { fileprivate(set) var emojify: Bool = false fileprivate(set) var isSourceTextExpanded: Bool = false fileprivate(set) var style: TelegramComposeAIMessageMode.StyleId = .neutral + var displayStyleTooltip: Bool = false fileprivate(set) var isProcessing: Bool = false { didSet { @@ -134,6 +136,8 @@ final class TextProcessingTranslateContentComponent: Component { private let targetText = ComponentView() private let separatorLayer: SimpleLayer + private var styleTooltip: (dimView: UIView, tooltip: ComponentView)? + private var component: TextProcessingTranslateContentComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false @@ -219,6 +223,16 @@ final class TextProcessingTranslateContentComponent: Component { } } } + + @objc private func onTooltipTapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + component.externalState.displayStyleTooltip = false + self.state?.updated(transition: .easeInOut(duration: 0.2)) + } + } func update(component: TextProcessingTranslateContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -449,8 +463,56 @@ final class TextProcessingTranslateContentComponent: Component { } contentHeight += bottomInset + + let size = CGSize(width: availableSize.width, height: contentHeight) + + if component.externalState.displayStyleTooltip, let sourceTextView = self.sourceText.view { + let tooltip: ComponentView + let dimView: UIView + var tooltipTransition = transition + if let current = self.styleTooltip { + tooltip = current.tooltip + dimView = current.dimView + } else { + tooltipTransition = tooltipTransition.withAnimation(.none) + tooltip = ComponentView() + dimView = UIView() + dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTooltipTapGesture(_:)))) + self.styleTooltip = (dimView, tooltip) + } + let tooltipSize = tooltip.update( + transition: tooltipTransition, + component: AnyComponent(TooltipComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Select Style", font: Font.regular(15.0), textColor: .white)) + )) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + transition.setFrame(view: dimView, frame: CGRect(origin: CGPoint(), size: size)) + let tooltipFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - tooltipSize.width) * 0.5), y: sourceTextView.frame.maxY + 12.0), size: tooltipSize) + if let tooltipView = tooltip.view as? TooltipComponent.View { + if tooltipView.superview == nil { + self.addSubview(dimView) + self.addSubview(tooltipView) + } + tooltipTransition.setFrame(view: tooltipView, frame: tooltipFrame) + tooltipView.updateBackground(relativeArrowTargetPosition: CGPoint(x: tooltipFrame.width * 0.5, y: 0.0)) + } + } else { + if let styleTooltip = self.styleTooltip { + self.styleTooltip = nil + styleTooltip.dimView.removeFromSuperview() + if let tooltipView = styleTooltip.tooltip.view { + transition.setAlpha(view: tooltipView, alpha: 0.0, completion: { [weak tooltipView] _ in + tooltipView?.removeFromSuperview() + }) + } + } + } - return CGSize(width: availableSize.width, height: contentHeight) + return size } } diff --git a/submodules/TelegramUI/Components/ToastComponent/BUILD b/submodules/TelegramUI/Components/ToastComponent/BUILD index 29d26d819c..b01b71e99d 100644 --- a/submodules/TelegramUI/Components/ToastComponent/BUILD +++ b/submodules/TelegramUI/Components/ToastComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/Display", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift index b534641c06..1e9fb3fe31 100644 --- a/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift +++ b/submodules/TelegramUI/Components/ToastComponent/Sources/ToastContentComponent.swift @@ -3,6 +3,7 @@ import UIKit import Display import ComponentFlow import ComponentDisplayAdapters +import GlassBackgroundComponent public final class ToastContentComponent: Component { public let icon: AnyComponent @@ -41,7 +42,8 @@ public final class ToastContentComponent: Component { public final class View: UIView { private var component: ToastContentComponent? - private let backgroundView: BlurredBackgroundView + private let backgroundContainerView: GlassBackgroundContainerView + private let backgroundView: GlassBackgroundView private let icon = ComponentView() private let content = ComponentView() @@ -54,11 +56,13 @@ public final class ToastContentComponent: Component { } override public init(frame: CGRect) { - self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.backgroundContainerView = GlassBackgroundContainerView() + self.backgroundView = GlassBackgroundView() super.init(frame: frame) - self.addSubview(self.backgroundView) + self.backgroundContainerView.contentView.addSubview(self.backgroundView) + self.addSubview(self.backgroundContainerView) } required init?(coder: NSCoder) { @@ -106,9 +110,13 @@ public final class ToastContentComponent: Component { } let size = CGSize(width: availableSize.width, height: contentHeight) - self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: .immediate) - self.backgroundView.update(size: size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + + let backgroundContainerInset: CGFloat = 64.0 + self.backgroundContainerView.update(size: CGRect(origin: CGPoint(), size: size).insetBy(dx: -backgroundContainerInset, dy: -backgroundContainerInset).size, isDark: true, transition: transition) + transition.setFrame(view: self.backgroundContainerView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -backgroundContainerInset, dy: -backgroundContainerInset)) + + self.backgroundView.update(size: size, cornerRadius: min(25.0, size.height * 0.5), isDark: true, tintColor: .init(kind: .panel), transition: transition) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: backgroundContainerInset, y: backgroundContainerInset), size: size)) return size } diff --git a/submodules/TelegramUI/Components/TooltipComponent/BUILD b/submodules/TelegramUI/Components/TooltipComponent/BUILD new file mode 100644 index 0000000000..38c81666ec --- /dev/null +++ b/submodules/TelegramUI/Components/TooltipComponent/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TooltipComponent", + module_name = "TooltipComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TooltipComponent/Sources/TooltipComponent.swift b/submodules/TelegramUI/Components/TooltipComponent/Sources/TooltipComponent.swift new file mode 100644 index 0000000000..92775511ab --- /dev/null +++ b/submodules/TelegramUI/Components/TooltipComponent/Sources/TooltipComponent.swift @@ -0,0 +1,140 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class TooltipComponent: Component { + public let content: AnyComponent + public let contentInsets: UIEdgeInsets + + public init( + content: AnyComponent, + contentInsets: UIEdgeInsets = UIEdgeInsets(top: 9.0, left: 14.0, bottom: 11.0, right: 14.0) + ) { + self.content = content + self.contentInsets = contentInsets + } + + public static func ==(lhs: TooltipComponent, rhs: TooltipComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.contentInsets != rhs.contentInsets { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private let backgroundShapeLayer: SimpleShapeLayer + private let content = ComponentView() + + private var component: TooltipComponent? + + private var backgroundFrame: CGRect? + + public override init(frame: CGRect) { + self.backgroundShapeLayer = SimpleShapeLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundShapeLayer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func updateBackground(relativeArrowTargetPosition: CGPoint) { + guard let backgroundFrame = self.backgroundFrame else { + return + } + + let cornerRadius: CGFloat = 12.0 + let verticalInset: CGFloat = 9.0 + let arrowWidth: CGFloat = 18.0 + let arrowTipRadius: CGFloat = 4.0 + let arrowBaseRadius: CGFloat = 3.0 + let sqrt2inv: CGFloat = 1.0 / sqrt(2.0) + + let arrowOnBottom = relativeArrowTargetPosition.y >= backgroundFrame.height * 0.5 + let arrowPosition = max(cornerRadius + arrowWidth / 2.0, min(backgroundFrame.width - cornerRadius - arrowWidth / 2.0, relativeArrowTargetPosition.x)) + + let topInset: CGFloat = arrowOnBottom ? 0.0 : verticalInset + let bottomInset: CGFloat = arrowOnBottom ? verticalInset : 0.0 + let totalHeight = backgroundFrame.height + verticalInset + + let path = UIBezierPath() + path.move(to: CGPoint(x: 0.0, y: topInset + cornerRadius)) + path.addArc(withCenter: CGPoint(x: cornerRadius, y: topInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: CGFloat(3.0 * CGFloat.pi / 2.0), clockwise: true) + if !arrowOnBottom { + path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0 - arrowBaseRadius, y: topInset)) + path.addQuadCurve(to: CGPoint(x: arrowPosition - arrowWidth / 2.0 + arrowBaseRadius * sqrt2inv, y: topInset - arrowBaseRadius * sqrt2inv), controlPoint: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: topInset)) + path.addLine(to: CGPoint(x: arrowPosition - arrowTipRadius * sqrt2inv, y: arrowTipRadius * sqrt2inv)) + path.addQuadCurve(to: CGPoint(x: arrowPosition + arrowTipRadius * sqrt2inv, y: arrowTipRadius * sqrt2inv), controlPoint: CGPoint(x: arrowPosition, y: 0.0)) + path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0 - arrowBaseRadius * sqrt2inv, y: topInset - arrowBaseRadius * sqrt2inv)) + path.addQuadCurve(to: CGPoint(x: arrowPosition + arrowWidth / 2.0 + arrowBaseRadius, y: topInset), controlPoint: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: topInset)) + } + path.addLine(to: CGPoint(x: backgroundFrame.width - cornerRadius, y: topInset)) + path.addArc(withCenter: CGPoint(x: backgroundFrame.width - cornerRadius, y: topInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat(3.0 * CGFloat.pi / 2.0), endAngle: 0.0, clockwise: true) + path.addLine(to: CGPoint(x: backgroundFrame.width, y: totalHeight - bottomInset - cornerRadius)) + path.addArc(withCenter: CGPoint(x: backgroundFrame.width - cornerRadius, y: totalHeight - bottomInset - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: CGFloat(CGFloat.pi / 2.0), clockwise: true) + if arrowOnBottom { + let arrowBaseY = totalHeight - bottomInset + path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0 + arrowBaseRadius, y: arrowBaseY)) + path.addQuadCurve(to: CGPoint(x: arrowPosition + arrowWidth / 2.0 - arrowBaseRadius * sqrt2inv, y: arrowBaseY + arrowBaseRadius * sqrt2inv), controlPoint: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: arrowBaseY)) + path.addLine(to: CGPoint(x: arrowPosition + arrowTipRadius * sqrt2inv, y: totalHeight - arrowTipRadius * sqrt2inv)) + path.addQuadCurve(to: CGPoint(x: arrowPosition - arrowTipRadius * sqrt2inv, y: totalHeight - arrowTipRadius * sqrt2inv), controlPoint: CGPoint(x: arrowPosition, y: totalHeight)) + path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0 + arrowBaseRadius * sqrt2inv, y: arrowBaseY + arrowBaseRadius * sqrt2inv)) + path.addQuadCurve(to: CGPoint(x: arrowPosition - arrowWidth / 2.0 - arrowBaseRadius, y: arrowBaseY), controlPoint: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: arrowBaseY)) + } + path.addLine(to: CGPoint(x: cornerRadius, y: totalHeight - bottomInset)) + path.addArc(withCenter: CGPoint(x: cornerRadius, y: totalHeight - bottomInset - cornerRadius), radius: cornerRadius, startAngle: CGFloat(CGFloat.pi / 2.0), endAngle: CGFloat.pi, clockwise: true) + path.close() + + self.backgroundShapeLayer.path = path.cgPath + self.backgroundShapeLayer.fillColor = UIColor(rgb: 0x2f2f2f).cgColor + self.backgroundShapeLayer.shadowColor = UIColor.black.cgColor + self.backgroundShapeLayer.shadowOpacity = 0.3 + self.backgroundShapeLayer.shadowRadius = 20.0 + self.backgroundShapeLayer.shadowOffset = CGSize(width: 0.0, height: 4.0) + self.backgroundShapeLayer.shadowPath = path.cgPath + + let shapeOriginY: CGFloat = arrowOnBottom ? backgroundFrame.origin.y : backgroundFrame.origin.y - verticalInset + self.backgroundShapeLayer.frame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x, y: shapeOriginY), size: CGSize(width: backgroundFrame.width, height: totalHeight)) + } + + func update(component: TooltipComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let contentSize = self.content.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: CGSize(width: availableSize.width - component.contentInsets.left - component.contentInsets.right, height: availableSize.height - component.contentInsets.top - component.contentInsets.bottom) + ) + + let backgroundSize = CGSize(width: component.contentInsets.left + component.contentInsets.right + contentSize.width, height: component.contentInsets.top + component.contentInsets.bottom + contentSize.height) + let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) + self.backgroundFrame = backgroundFrame + + let contentFrame = CGRect(origin: CGPoint(x: component.contentInsets.left, y: component.contentInsets.top), size: contentSize) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: contentFrame) + } + + return backgroundSize + } + } + + 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) + } +}