Translation improvements

This commit is contained in:
Isaac 2026-03-22 05:17:29 +03:00
parent 9110dda4b4
commit b2ca658489
14 changed files with 557 additions and 113 deletions

View file

@ -211,7 +211,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
}
private let dimView: UIView
private let containerView: UIView
public let containerView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let bottomContainer: SparseContainerView
@ -505,10 +505,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.containerView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
@ -518,20 +515,14 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
if let initialVelocity = initialVelocity {
let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
transition.updatePosition(layer: self.scrollContentClippingView.layer, position: CGPoint(x: self.scrollContentClippingView.layer.position.x, y: self.scrollContentClippingView.layer.position.y + animateOffset), completion: { _ in
transition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.layer.position.x, y: self.containerView.layer.position.y + animateOffset), completion: { _ in
completion()
})
transition.updatePosition(layer: self.backgroundLayer, position: CGPoint(x: self.backgroundLayer.position.x, y: self.backgroundLayer.position.y + animateOffset))
transition.updatePosition(layer: self.navigationBarContainer.layer, position: CGPoint(x: self.navigationBarContainer.layer.position.x, y: self.navigationBarContainer.layer.position.y + animateOffset))
transition.updatePosition(layer: self.bottomContainer.layer, position: CGPoint(x: self.bottomContainer.layer.position.x, y: self.bottomContainer.layer.position.y + animateOffset))
} else {
let duration: Double = 0.25
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
self.containerView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
}

View file

@ -207,6 +207,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case giftAuctionTips = 84
case giftCraftingTips = 85
case copyProtectionTips = 86
case aiTextProcessingStyleSelectionTips = 87
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -584,6 +585,10 @@ private struct ApplicationSpecificNoticeKeys {
static func copyProtectionTips() -> 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<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
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)
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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)
}

View file

@ -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<ViewControllerComponentContainer.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<Empty>?
private var languageSelectionMenu: ComponentView<Empty>?
private let animateOut = ActionSlot<Action<Void>>()
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<Empty>
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<Void>()
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<ViewControllerComponentContainer.Environment>.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<Empty>
@ -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<Empty>] = []
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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)

View file

@ -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 {

View file

@ -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<Empty>()
private let separatorLayer: SimpleLayer
private var styleTooltip: (dimView: UIView, tooltip: ComponentView<Empty>)?
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<Empty>, 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<Empty>
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
}
}

View file

@ -13,6 +13,7 @@ swift_library(
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",

View file

@ -3,6 +3,7 @@ import UIKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
import GlassBackgroundComponent
public final class ToastContentComponent: Component {
public let icon: AnyComponent<Empty>
@ -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<Empty>()
private let content = ComponentView<Empty>()
@ -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
}

View file

@ -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",
],
)

View file

@ -0,0 +1,140 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class TooltipComponent: Component {
public let content: AnyComponent<Empty>
public let contentInsets: UIEdgeInsets
public init(
content: AnyComponent<Empty>,
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<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}