mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Translation improvements
This commit is contained in:
parent
9110dda4b4
commit
b2ca658489
14 changed files with 557 additions and 113 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ swift_library(
|
|||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
19
submodules/TelegramUI/Components/TooltipComponent/BUILD
Normal file
19
submodules/TelegramUI/Components/TooltipComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue