diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 20679c4941..cacc0bc915 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7365,3 +7365,38 @@ Sorry for the inconvenience."; "CreateExternalStream.StreamKey" = "stream key"; "CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below."; "CreateExternalStream.StartStreaming" = "Start Streaming"; + +"Translate.Title" = "Translate"; +"Translate.CopyTranslation" = "Copy Translation"; +"Translate.ChangeLanguage" = "Change Language"; +"Translate.More" = "more"; +"Translate.Languages.Title" = "Languages"; +"Translate.Languages.Original" = "Original"; +"Translate.Languages.Translation" = "Translation"; + +"Bot.AddToChat" = "Add to Group or Channel"; +"Bot.AddToChatInfo" = "This bot is able to manage a group or channel."; + +"Bot.AddToChat.Title" = "Add to Group or Channel"; +"Bot.AddToChat.MyChannels" = "CHANNEL I MANAGE"; +"Bot.AddToChat.MyGroups" = "GROUPS I MANAGE"; +"Bot.AddToChat.OtherGroups" = "GROUPS"; + +"Bot.AddToChat.Add.Title" = "Add Bot"; +"Bot.AddToChat.Add.AdminRights" = "Admin Rights"; +"Bot.AddToChat.Add.AddAsAdmin" = "Add Bot as Admin"; +"Bot.AddToChat.Add.AddAsMember" = "Add Bot as Member"; + +"Bot.AddToChat.Add.AdminAlertTitle" = "Add Bot as Admin?"; +"Bot.AddToChat.Add.AdminAlertTextGroup" = "Are you sure you want to add the bot as an admin in the group **%@**?"; +"Bot.AddToChat.Add.AdminAlertTextChannel" = "Are you sure you want to add the bot as an admin in the channel **%@**?"; +"Bot.AddToChat.Add.AdminAlertAdd" = "Add as Admin"; + +"Bot.AddToChat.Add.MemberAlertTitle" = "Add Bot as Member?"; +"Bot.AddToChat.Add.MemberAlertTextGroup" = "Are you sure you want to add the bot as a member in the group **%@**?"; +"Bot.AddToChat.Add.MemberAlertTextChannel" = "Are you sure you want to add the bot as a member in the channel **%@**?"; +"Bot.AddToChat.Add.MemberAlertAdd" = "Add as Member"; + +"PeerInfo.ButtonStop" = "Stop"; + +"Localization.ShowTranslateInfoExtended" = "Show 'Translate' button in the message context menu.\n\nGoogle may have access to the messages you translate."; diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 6f85142c6b..f5b6581a5a 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -1143,7 +1143,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string return (current, inputMode) } - speakText(text) + let _ = speakText(text) if #available(iOS 13.0, *) { UIMenuController.shared.hideMenu() diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 655396d0dd..2ef790f35d 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -255,7 +255,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { let topInset: CGFloat = edgeTopInset var dismissing = false - if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 600.0) { + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { self.interactivelyDismissed?() dismissing = true } else if self.isExpanded { diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 660b99f8bc..a5850d57c8 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -279,6 +279,9 @@ public class AttachmentController: ViewController { } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + guard !self.isDismissing else { + return + } if case .ended = recognizer.state { if let controller = self.currentControllers.last { controller.requestDismiss(completion: { [weak self] in @@ -436,9 +439,9 @@ public class AttachmentController: ViewController { self.animating = true if case .regular = layout.metrics.widthClass { self.layer.allowsGroupOpacity = true - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - let _ = self.container.dismiss(transition: .immediate, completion: completion) - self.animating = false + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in + let _ = self?.container.dismiss(transition: .immediate, completion: completion) + self?.animating = false }) } else { let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -515,7 +518,6 @@ public class AttachmentController: ViewController { self.wrapperNode.view.mask = nil } - let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting) let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, transition: transition) var panelTransition = transition @@ -583,6 +585,10 @@ public class AttachmentController: ViewController { } } + deinit { + print() + } + public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -596,11 +602,15 @@ public class AttachmentController: ViewController { self.displayNodeDidLoad() } + public func _dismiss() { + super.dismiss(animated: false, completion: {}) + } + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) if flag { - self.node.animateOut(completion: { - super.dismiss(animated: false, completion: {}) + self.node.animateOut(completion: { [weak self] in + self?._dismiss() completion?() }) } else { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index dfd74223bf..ec45b3a08a 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -275,7 +275,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } } strongSelf.editableControlNode = editableControlNode - strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode) + strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.containerNode) editableControlNode.frame = editableControlFrame transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 68680643de..8c635b8041 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -5,6 +5,7 @@ public final class Button: Component { public let content: AnyComponent public let minSize: CGSize? public let tag: AnyObject? + public let automaticHighlight: Bool public let action: () -> Void convenience public init( @@ -15,6 +16,7 @@ public final class Button: Component { content: content, minSize: nil, tag: nil, + automaticHighlight: true, action: action ) } @@ -23,11 +25,13 @@ public final class Button: Component { content: AnyComponent, minSize: CGSize?, tag: AnyObject? = nil, + automaticHighlight: Bool = true, action: @escaping () -> Void ) { self.content = content self.minSize = nil self.tag = tag + self.automaticHighlight = automaticHighlight self.action = action } @@ -36,6 +40,7 @@ public final class Button: Component { content: self.content, minSize: minSize, tag: self.tag, + automaticHighlight: self.automaticHighlight, action: self.action ) } @@ -45,6 +50,7 @@ public final class Button: Component { content: self.content, minSize: self.minSize, tag: tag, + automaticHighlight: self.automaticHighlight, action: self.action ) } @@ -59,6 +65,9 @@ public final class Button: Component { if lhs.tag !== rhs.tag { return false } + if lhs.automaticHighlight != rhs.automaticHighlight { + return false + } return true } @@ -68,6 +77,9 @@ public final class Button: Component { private var component: Button? private var currentIsHighlighted: Bool = false { didSet { + guard let component = self.component, component.automaticHighlight else { + return + } if self.currentIsHighlighted != oldValue { self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 } diff --git a/submodules/ComponentFlow/Source/Components/Circle.swift b/submodules/ComponentFlow/Source/Components/Circle.swift index ed5f8a9f34..cb770423f0 100644 --- a/submodules/ComponentFlow/Source/Components/Circle.swift +++ b/submodules/ComponentFlow/Source/Components/Circle.swift @@ -2,26 +2,31 @@ import Foundation import UIKit public final class Circle: Component { - public let color: UIColor + public let fillColor: UIColor + public let strokeColor: UIColor + public let strokeWidth: CGFloat public let size: CGSize - public let width: CGFloat - public init(color: UIColor, size: CGSize, width: CGFloat) { - self.color = color + public init(fillColor: UIColor = .clear, strokeColor: UIColor = .clear, strokeWidth: CGFloat = 0.0, size: CGSize) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.strokeWidth = strokeWidth self.size = size - self.width = width } public static func ==(lhs: Circle, rhs: Circle) -> Bool { - if !lhs.color.isEqual(rhs.color) { + if !lhs.fillColor.isEqual(rhs.fillColor) { + return false + } + if !lhs.strokeColor.isEqual(rhs.strokeColor) { + return false + } + if lhs.strokeWidth != rhs.strokeWidth { return false } if lhs.size != rhs.size { return false } - if lhs.width != rhs.width { - return false - } return true } @@ -38,9 +43,13 @@ public final class Circle: Component { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) if let context = UIGraphicsGetCurrentContext() { - context.setStrokeColor(component.color.cgColor) - context.setLineWidth(component.width) - context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.width / 2.0, dy: component.width / 2.0)) + context.setFillColor(component.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + if component.strokeWidth > 0.0 { + context.setStrokeColor(component.strokeColor.cgColor) + context.setLineWidth(component.strokeWidth) + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.strokeWidth / 2.0, dy: component.strokeWidth / 2.0)) + } } self.image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() diff --git a/submodules/ComponentFlow/Source/Gestures/LongPressGesture.swift b/submodules/ComponentFlow/Source/Gestures/LongPressGesture.swift new file mode 100644 index 0000000000..6bc11621cf --- /dev/null +++ b/submodules/ComponentFlow/Source/Gestures/LongPressGesture.swift @@ -0,0 +1,59 @@ +import Foundation +import UIKit + +public extension Gesture { + enum LongPressGestureState { + case began + case ended + } + + private final class LongPressGesture: Gesture { + private class Impl: UILongPressGestureRecognizer { + var action: (LongPressGestureState) -> Void + + init(pressDuration: Double, action: @escaping (LongPressGestureState) -> Void) { + self.action = action + + super.init(target: nil, action: nil) + self.minimumPressDuration = pressDuration + self.addTarget(self, action: #selector(self.onAction)) + } + + @objc private func onAction() { + switch self.state { + case .began: + self.action(.began) + case .ended, .cancelled: + self.action(.ended) + default: + break + } + } + } + + static let id = Id() + + private let pressDuration: Double + private let action: (LongPressGestureState) -> Void + + init(pressDuration: Double, action: @escaping (LongPressGestureState) -> Void) { + self.pressDuration = pressDuration + self.action = action + + super.init(id: Self.id) + } + + override func create() -> UIGestureRecognizer { + return Impl(pressDuration: self.pressDuration, action: self.action) + } + + override func update(gesture: UIGestureRecognizer) { + (gesture as! Impl).minimumPressDuration = self.pressDuration + (gesture as! Impl).action = self.action + } + } + + static func longPress(duration: Double = 0.2, _ action: @escaping (LongPressGestureState) -> Void) -> Gesture { + return LongPressGesture(pressDuration: duration, action: action) + } +} diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index e481573ddd..6e3faf3711 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -32,8 +32,8 @@ public final class ComponentHostView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, containerSize: CGSize) -> CGSize { - let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: containerSize) + public func update(transition: Transition, component: AnyComponent, @EnvironmentBuilder environment: () -> Environment, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize { + let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize) self.currentSize = size return size } diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 3699b75edb..1e99f867c5 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -10,6 +10,7 @@ public final class MultilineTextComponent: Component { public var truncationType: CTLineTruncationType public var maximumNumberOfLines: Int public var lineSpacing: CGFloat + public var cutout: TextNodeCutout? public var insets: UIEdgeInsets public var textShadowColor: UIColor? public var textStroke: (UIColor, CGFloat)? @@ -21,6 +22,7 @@ public final class MultilineTextComponent: Component { truncationType: CTLineTruncationType = .end, maximumNumberOfLines: Int = 1, lineSpacing: CGFloat = 0.0, + cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil @@ -31,6 +33,7 @@ public final class MultilineTextComponent: Component { self.truncationType = truncationType self.maximumNumberOfLines = maximumNumberOfLines self.lineSpacing = lineSpacing + self.cutout = cutout self.insets = insets self.textShadowColor = textShadowColor self.textStroke = textStroke @@ -55,6 +58,9 @@ public final class MultilineTextComponent: Component { if lhs.lineSpacing != rhs.lineSpacing { return false } + if lhs.cutout != rhs.cutout { + return false + } if lhs.insets != rhs.insets { return false } @@ -93,7 +99,7 @@ public final class MultilineTextComponent: Component { alignment: component.horizontalAlignment, verticalAlignment: component.verticalAlignment, lineSpacing: component.lineSpacing, - cutout: nil, + cutout: component.cutout, insets: component.insets, textShadowColor: component.textShadowColor, textStroke: component.textStroke, diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 50f6cc5a8f..fd2367351d 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -21,6 +21,15 @@ public extension Transition.Animation.Curve { self = .spring } } + + var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { + switch self { + case .easeInOut: + return .easeInOut + case .spring: + return .spring + } + } } public extension Transition { @@ -32,6 +41,15 @@ public extension Transition { self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) } } + + var containedViewLayoutTransition: ContainedViewLayoutTransition { + switch self.animation { + case .none: + return .immediate + case let .curve(duration, curve): + return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve) + } + } } open class ViewControllerComponentContainer: ViewController { @@ -125,7 +143,7 @@ open class ViewControllerComponentContainer: ViewController { let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, - safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.intrinsicInsets.left + layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.intrinsicInsets.right + layout.safeInsets.right), + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), isVisible: self.currentIsVisible, theme: self.presentationData.theme, strings: self.presentationData.strings, diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index a6c17d41c3..059c716c1a 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -848,6 +848,11 @@ open class NavigationController: UINavigationController, ContainableController, } } + if self._keepModalDismissProgress { + modalStyleOverlayTransitionFactor = 0.0 + self._keepModalDismissProgress = false + } + topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor) switch layout.metrics.widthClass { @@ -1358,6 +1363,7 @@ open class NavigationController: UINavigationController, ContainableController, self._viewControllersPromise.set(self.viewControllers) } + public var _keepModalDismissProgress = false public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) { let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in guard let strongSelf = self else { @@ -1385,6 +1391,7 @@ open class NavigationController: UINavigationController, ContainableController, } } } + strongSelf.updateContainersNonReentrant(transition: .immediate) }, statusBarUpdated: { [weak self] transition in guard let strongSelf = self else { diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 71887c2ba2..20c7848a0b 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -39,7 +39,7 @@ swift_library( "//submodules/Speak:Speak", "//submodules/UndoUI:UndoUI", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", - "//submodules/Translate:Translate", + "//submodules/TranslateUI:TranslateUI", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 4f8de99440..05a27ec5d9 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -16,7 +16,7 @@ import PresentationDataUtils import ImageContentAnalysis import TextSelectionNode import Speak -import Translate +import TranslateUI import ShareController import UndoUI @@ -352,9 +352,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { window.rootViewController?.present(controller, animated: true) } case .speak: - speakText(string) + let _ = speakText(string) case .translate: - translateText(context: strongSelf.context, text: string) + if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController { + let controller = TranslateScreen(context: strongSelf.context, text: string, fromLanguage: nil) + parentController.present(controller, in: .window(.root)) + } } }) recognizedContentNode.barcodeAction = { [weak self] payload, rect in diff --git a/submodules/InstantPageUI/BUILD b/submodules/InstantPageUI/BUILD index 3cc5b21b3a..6b849ce87a 100644 --- a/submodules/InstantPageUI/BUILD +++ b/submodules/InstantPageUI/BUILD @@ -25,7 +25,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/LocationResources:LocationResources", "//submodules/UndoUI:UndoUI", - "//submodules/Translate:Translate", + "//submodules/TranslateUI:TranslateUI", ], visibility = [ "//visibility:public", diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 8d94ea3372..7b0521d624 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -16,7 +16,7 @@ import OpenInExternalAppUI import LocationUI import UndoUI import ContextUI -import Translate +import TranslateUI final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private weak var controller: InstantPageController? @@ -1048,8 +1048,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) if canTranslate { - actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { - translateText(context: context, text: text, fromLang: language) + actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in + let controller = TranslateScreen(context: context, text: text, fromLanguage: language) + self?.present(controller, nil) })) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6c5d9679a1..8fe209920a 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -21,8 +21,6 @@ import SparseItemGrid import UndoUI import PresentationDataUtils -let overflowInset: CGFloat = 0.0 - final class MediaPickerInteraction { let openMedia: (PHFetchResult, Int, UIImage?) -> Void let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void @@ -248,7 +246,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.gridNode.visibleContentOffsetChanged = { [weak self] _ in self?.updateNavigation(transition: .immediate) - self?.updateScrollingArea() } self.hiddenMediaDisposable = (self.hiddenMediaId.get() @@ -325,7 +322,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if self.controller?.collection != nil { self.gridNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in - return point.x > 44.0 + overflowInset + return point.x > 44.0 } } @@ -380,7 +377,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } if self.controller?.collection != nil { - self.selectionGesture?.sideInset = 44.0 + overflowInset + self.selectionGesture?.sideInset = 44.0 } } @@ -406,14 +403,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { guard let (layout, _) = self.validLayout else { return } - + var tag: Int32? self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { tag = itemNode.tag } } - + let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) } if self.currentScrollingTag != tag { self.currentScrollingTag = tag @@ -421,7 +418,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.scrollingArea.feedbackTap() } } - + self.scrollingArea.update( containerSize: layout.size, containerInsets: self.gridNode.gridLayout.insets, @@ -848,7 +845,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { insets.top += navigationBarHeight let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) - let innerBounds = CGRect(origin: CGPoint(x: -overflowInset, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)) + let innerBounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) let itemsPerRow: Int if case .compact = layout.metrics.widthClass { @@ -955,7 +952,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { transition.updateFrame(node: self.backgroundNode, frame: innerBounds) self.backgroundNode.update(size: bounds.size, transition: transition) - transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: overflowInset, y: 0.0), size: CGSize(width: bounds.width - overflowInset * 2.0, height: bounds.height))) + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: bounds.height))) self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemWidth, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in guard let strongSelf = self else { @@ -1060,6 +1057,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } private let groupedPromise = ValuePromise(true) + private var isDismissing = false + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?, collection: PHAssetCollection? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil) { self.context = context @@ -1160,7 +1159,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in - if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState { + if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing { + strongSelf.isDismissing = true if let currentItem = currentItem { selectionState.setItem(currentItem, selected: true) } @@ -1285,11 +1285,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public func requestDismiss(completion: @escaping () -> Void) { if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 { + self.isDismissing = true let controller = textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Attachment_CancelSelectionAlertText, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertNo, action: { }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, action: { completion() })]) + controller.dismissed = { [weak self] in + self?.isDismissing = false + } self.present(controller, in: .window(.root)) } else { completion() diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 8328190984..56f591662a 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -93,7 +93,7 @@ swift_library( "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", "//submodules/WebPBinding:WebPBinding", "//submodules/Components/ReactionImageComponent:ReactionImageComponent", - "//submodules/Translate:Translate", + "//submodules/TranslateUI:TranslateUI", "//submodules/QrCodeUI:QrCodeUI", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index c3a778db4b..c480f1bcd9 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -15,7 +15,7 @@ import SearchBarNode import SearchUI import UndoUI import TelegramUIPreferences -import Translate +import TranslateUI private enum LanguageListSection: ItemListSectionId { case translate @@ -470,7 +470,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value)) entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo)) } else { - entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo)) + entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfoExtended)) } } @@ -520,7 +520,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { if let strongSelf = self { strongSelf.push(translationSettingsController(context: strongSelf.context)) } - }, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil)) + }, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) != entries.count, crossfade: (previousState == nil) != (localizationListState == nil)) strongSelf.enqueueTransition(transition) }) self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() diff --git a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift index d3df163669..bccbe7f287 100644 --- a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -10,7 +10,7 @@ import ItemListUI import PresentationDataUtils import TelegramStringFormatting import AccountContext -import Translate +import TranslateUI private final class TranslationSettingsControllerArguments { let context: AccountContext @@ -112,10 +112,10 @@ public func translationSettingsController(context: AccountContext) -> ViewContro }).start() }) - let enLocale = Locale(identifier: "en") var languages: [(String, String, String)] = [] - for code in supportedTranslationLanguages { + var addedLanguages = Set() + for code in popularTranslationLanguages { if let title = enLocale.localizedString(forLanguageCode: code) { let languageLocale = Locale(identifier: code) let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title @@ -125,6 +125,20 @@ public func translationSettingsController(context: AccountContext) -> ViewContro } else { languages.append(value) } + addedLanguages.insert(code) + } + } + + for code in supportedTranslationLanguages { + if !addedLanguages.contains(code), let title = enLocale.localizedString(forLanguageCode: code) { + let languageLocale = Locale(identifier: code) + let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + let value = (code, title.capitalized, subtitle.capitalized) + if code == interfaceLanguageCode { + languages.insert(value, at: 0) + } else { + languages.append(value) + } } } diff --git a/submodules/Speak/Sources/Speak.swift b/submodules/Speak/Sources/Speak.swift index 1afd04258f..0a795344cb 100644 --- a/submodules/Speak/Sources/Speak.swift +++ b/submodules/Speak/Sources/Speak.swift @@ -5,9 +5,31 @@ import AVFoundation private final class LinkHelperClass: NSObject { } -public func speakText(_ text: String) { +public class SpeechSynthesizerHolder: NSObject, AVSpeechSynthesizerDelegate { + private var speechSynthesizer: AVSpeechSynthesizer + + public var completion: () -> Void = {} + + init(speechSynthesizer: AVSpeechSynthesizer) { + self.speechSynthesizer = speechSynthesizer + + super.init() + + self.speechSynthesizer.delegate = self + } + + public func stop() { + self.speechSynthesizer.stopSpeaking(at: .immediate) + } + + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { + self.completion() + } +} + +public func speakText(_ text: String) -> SpeechSynthesizerHolder? { guard !text.isEmpty else { - return + return nil } let speechSynthesizer = AVSpeechSynthesizer() let utterance = AVSpeechUtterance(string: text) @@ -15,4 +37,6 @@ public func speakText(_ text: String) { utterance.voice = AVSpeechSynthesisVoice(language: language) } speechSynthesizer.speak(utterance) + + return SpeechSynthesizerHolder(speechSynthesizer: speechSynthesizer) } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 5df799a69a..0a7201f99f 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -641,9 +641,9 @@ public final class MediaStreamComponent: CombinedComponent { navigationRightItems.append(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button( content: AnyComponent(ZStack([ AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( - color: .white, - size: CGSize(width: 22.0, height: 22.0), - width: 1.5 + strokeColor: .white, + strokeWidth: 1.5, + size: CGSize(width: 22.0, height: 22.0) ))), AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.Animation( diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 16cd09f66f..1f7e2ebeff 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -537,7 +537,9 @@ private final class CallSessionManagerContext { guard let strongSelf = self else { return } - strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) + strongSelf.contexts.removeValue(forKey: internalId) + strongSelf.contextIdByStableId.removeValue(forKey: stableId) + strongSelf.ringingStatesUpdated() } })) self.contextIdByStableId[stableId] = internalId diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 64376ef2c1..a233f391a3 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -30,15 +30,16 @@ private func generateInstantVideoBackground(fillColor: UIColor, strokeColor: UIC private func generateActionPhotoBackground(fillColor: UIColor, strokeColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 214.0, height: 214.0), rotatedContext: { size, context in let lineWidth: CGFloat = 0.5 + let cornerRadius: CGFloat = 16.0 context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(strokeColor.cgColor) - let strokePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 15.0) + let strokePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius) context.addPath(strokePath.cgPath) context.fillPath() context.setFillColor(fillColor.cgColor) - let fillPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0)), cornerRadius: 15.0) + let fillPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0)), cornerRadius: cornerRadius) context.addPath(fillPath.cgPath) context.fillPath() }) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 8c1aa97946..6d619a89b5 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -252,7 +252,6 @@ swift_library( "//submodules/QrCodeUI:QrCodeUI", "//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent", "//submodules/Components/ReactionImageComponent:ReactionImageComponent", - "//submodules/Translate:Translate", "//submodules/TabBarUI:TabBarUI", "//submodules/SoftwareVideo:SoftwareVideo", "//submodules/ManagedFile:ManagedFile", @@ -266,6 +265,7 @@ swift_library( "//submodules/MediaPickerUI:MediaPickerUI", "//submodules/ChatMessageBackground:ChatMessageBackground", "//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen", + "//submodules/TranslateUI:TranslateUI", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 796bffaece..c34741e081 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -60,7 +60,7 @@ import InviteLinksUI import Markdown import TelegramPermissionsUI import Speak -import Translate +import TranslateUI import UniversalMediaPlayer import WallpaperBackgroundNode import ChatListUI @@ -2915,7 +2915,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G window.rootViewController?.present(controller, animated: true) } case .speak: - speakText(text.string) + let _ = speakText(text.string) case .translate: let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1) @@ -2929,7 +2929,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) - translateText(context: context, text: text.string, fromLang: language) + let controller = TranslateScreen(context: context, text: text.string, fromLanguage: language) + controller.pushController = { [weak self] c in + self?.effectiveNavigationController?._keepModalDismissProgress = true + self?.push(c) + } + controller.presentController = { [weak self] c in + self?.present(c, in: .window(.root)) + } + strongSelf.present(controller, in: .window(.root)) }) } }, displayImportedMessageTooltip: { [weak self] _ in @@ -10630,7 +10638,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if inputIsActive { - Queue.mainQueue().after(0.1, { + Queue.mainQueue().after(0.15, { present() }) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index b4ef4f108d..e6a3441fce 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -25,7 +25,7 @@ import AdUI import TelegramNotices import ReactionListContextMenuContent import TelegramUIPreferences -import Translate +import TranslateUI import DebugSettingsUI import ChatPresentationInterfaceState import Pasteboard diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 0892e0dcde..d5af528036 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -2316,7 +2316,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string return (current, inputMode) } - speakText(text) + let _ = speakText(text) if #available(iOS 13.0, *) { UIMenuController.shared.hideMenu() diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index fc65e751bb..c89a778cf0 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -492,7 +492,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } } let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController) - present(controller, nil) + Queue.mainQueue().after(0.3) { + present(controller, nil) + } } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 285a0d770b..64ee493e68 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -63,7 +63,7 @@ import PasswordSetupUI import CalendarMessageScreen import TooltipUI import QrCodeUI -import Translate +import TranslateUI import ChatPresentationInterfaceState import CreateExternalMediaStreamScreen @@ -5017,8 +5017,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) if canTranslate { - actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: { - translateText(context: context, text: text, fromLang: language) + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: { [weak self] in + + let controller = TranslateScreen(context: context, text: text, fromLanguage: language) + self?.controller?.present(controller, in: .window(.root)) })) } diff --git a/submodules/Translate/BUILD b/submodules/Translate/BUILD deleted file mode 100644 index a6dcb04479..0000000000 --- a/submodules/Translate/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "Translate", - module_name = "Translate", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/Display:Display", - "//submodules/AccountContext:AccountContext", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift deleted file mode 100644 index 8c22ac3417..0000000000 --- a/submodules/Translate/Sources/Translate.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation -import UIKit -import Display -import AccountContext -import NaturalLanguage -import TelegramCore - -// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker -private final class LinkHelperClass: NSObject { -} - -public var supportedTranslationLanguages = [ - "en", - "ar", - "zh-Hans", - "zh-Hant", - "fr", - "de", - "it", - "ja", - "ko", - "pt", - "ru", - "es" -] - -@available(iOS 12.0, *) -private let languageRecognizer = NLLanguageRecognizer() - -public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) { - guard showTranslate, text.count > 0 else { - return (false, nil) - } - - if #available(iOS 15.0, *) { - var dontTranslateLanguages: [String] = [] - if let ignoredLanguages = ignoredLanguages { - dontTranslateLanguages = ignoredLanguages - } else { - dontTranslateLanguages = [context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode] - } - - let text = String(text.prefix(64)) - languageRecognizer.processString(text) - let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3) - languageRecognizer.reset() - - let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value }) - if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) { - return (!dontTranslateLanguages.contains(language.key.rawValue), language.key.rawValue) - } else { - return (false, nil) - } - } else { - return (false, nil) - } -} - -public func translateText(context: AccountContext, text: String, fromLang: String? = nil) { - guard !text.isEmpty else { - return - } - if #available(iOS 15.0, *) { - let text = text.unicodeScalars.filter { !$0.properties.isEmojiPresentation }.reduce("") { $0 + String($1) } - - let textView = UITextView() - textView.text = text - textView.isEditable = false - if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { - topController.view.addSubview(textView) - textView.selectAll(nil) - textView.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil) - - DispatchQueue.main.async { - textView.removeFromSuperview() - } - } - - let toLang = context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode - let _ = context.engine.messages.translate(text: text, fromLang: fromLang, toLang: toLang).start() - } -} diff --git a/submodules/TranslateUI/BUILD b/submodules/TranslateUI/BUILD new file mode 100644 index 0000000000..537f55593b --- /dev/null +++ b/submodules/TranslateUI/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TranslateUI", + module_name = "TranslateUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/AlertUI:AlertUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/Speak:Speak", + "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/ItemListUI:ItemListUI", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/BundleIconComponent:BundleIconComponent", + "//submodules/UndoUI:UndoUI", + "//submodules/ActivityIndicator:ActivityIndicator", + "//submodules/ChatListSearchItemNode:ChatListSearchItemNode", + "//submodules/ShimmerEffect:ShimmerEffect", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TranslateUI/Sources/LanguageSelectionController.swift b/submodules/TranslateUI/Sources/LanguageSelectionController.swift new file mode 100644 index 0000000000..2bf089e528 --- /dev/null +++ b/submodules/TranslateUI/Sources/LanguageSelectionController.swift @@ -0,0 +1,194 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import TelegramStringFormatting +import AccountContext + +private final class LanguageSelectionControllerArguments { + let context: AccountContext + let updateLanguageSelected: (String) -> Void + + init(context: AccountContext, updateLanguageSelected: @escaping (String) -> Void) { + self.context = context + self.updateLanguageSelected = updateLanguageSelected + } +} + +private enum LanguageSelectionControllerSection: Int32 { + case languages +} + +private enum LanguageSelectionControllerEntry: ItemListNodeEntry { + case language(Int32, PresentationTheme, String, String, Bool, String) + + var section: ItemListSectionId { + switch self { + case .language: + return LanguageSelectionControllerSection.languages.rawValue + } + } + + var stableId: Int32 { + switch self { + case let .language(index, _, _, _, _, _): + return index + } + } + + static func ==(lhs: LanguageSelectionControllerEntry, rhs: LanguageSelectionControllerEntry) -> Bool { + switch lhs { + case let .language(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsValue, lhsCode): + if case let .language(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsValue, rhsCode) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsValue == rhsValue, lhsCode == rhsCode { + return true + } else { + return false + } + } + } + + static func <(lhs: LanguageSelectionControllerEntry, rhs: LanguageSelectionControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! LanguageSelectionControllerArguments + switch self { + case let .language(_, _, title, subtitle, value, code): + return LocalizationListItem(presentationData: presentationData, id: code, title: title, subtitle: subtitle, checked: value, activity: false, loading: false, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: self.section, alwaysPlain: false, action: { + arguments.updateLanguageSelected(code) + }, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }) + } + } +} + +private func languageSelectionControllerEntries(theme: PresentationTheme, strings: PresentationStrings, selectedLanguage: String, languages: [(String, String, String)]) -> [LanguageSelectionControllerEntry] { + var entries: [LanguageSelectionControllerEntry] = [] + + var index: Int32 = 0 + for (code, title, subtitle) in languages { + entries.append(.language(index, theme, title, subtitle, code == selectedLanguage, code)) + index += 1 + } + + return entries +} + +private struct LanguageSelectionControllerState: Equatable { + enum Section { + case original + case translation + } + + var section: Section + var fromLanguage: String + var toLanguage: String +} + +public func languageSelectionController(context: AccountContext, fromLanguage: String, toLanguage: String, completion: @escaping (String, String) -> Void) -> ViewController { + let statePromise = ValuePromise(LanguageSelectionControllerState(section: .translation, fromLanguage: fromLanguage, toLanguage: toLanguage), ignoreRepeated: true) + let stateValue = Atomic(value: LanguageSelectionControllerState(section: .translation, fromLanguage: fromLanguage, toLanguage: toLanguage)) + let updateState: ((LanguageSelectionControllerState) -> LanguageSelectionControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let actionsDisposable = DisposableSet() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let interfaceLanguageCode = presentationData.strings.baseLanguageCode + + var dismissImpl: (() -> Void)? + + let arguments = LanguageSelectionControllerArguments(context: context, updateLanguageSelected: { code in + updateState { current in + var updated = current + switch updated.section { + case .original: + updated.fromLanguage = code + case .translation: + updated.toLanguage = code + } + return updated + } + }) + + let enLocale = Locale(identifier: "en") + var languages: [(String, String, String)] = [] + var addedLanguages = Set() + for code in popularTranslationLanguages { + if let title = enLocale.localizedString(forLanguageCode: code) { + let languageLocale = Locale(identifier: code) + let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + let value = (code, title.capitalized, subtitle.capitalized) + if code == interfaceLanguageCode { + languages.insert(value, at: 0) + } else { + languages.append(value) + } + addedLanguages.insert(code) + } + } + + for code in supportedTranslationLanguages { + if !addedLanguages.contains(code), let title = enLocale.localizedString(forLanguageCode: code) { + let languageLocale = Locale(identifier: code) + let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + let value = (code, title.capitalized, subtitle.capitalized) + if code == interfaceLanguageCode { + languages.insert(value, at: 0) + } else { + languages.append(value) + } + } + } + + let signal = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Translate_Languages_Original, presentationData.strings.Translate_Languages_Translation], 1), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + completion(state.fromLanguage, state.toLanguage) + dismissImpl?() + }), backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + + let selectedLanguage: String + switch state.section { + case.original: + selectedLanguage = state.fromLanguage + case .translation: + selectedLanguage = state.toLanguage + } + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: languageSelectionControllerEntries(theme: presentationData.theme, strings: presentationData.strings, selectedLanguage: selectedLanguage, languages: languages), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.titleControlValueChanged = { value in + updateState { current in + var updated = current + if value == 0 { + updated.section = .original + } else { + updated.section = .translation + } + return updated + } + } + controller.alwaysSynchronous = true + controller.navigationPresentation = .modal + + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true, completion: nil) + } + + return controller +} diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift b/submodules/TranslateUI/Sources/LocalizationListItem.swift similarity index 94% rename from submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift rename to submodules/TranslateUI/Sources/LocalizationListItem.swift index d42d43d27b..4ea85cc9d7 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift +++ b/submodules/TranslateUI/Sources/LocalizationListItem.swift @@ -10,14 +10,21 @@ import ActivityIndicator import ChatListSearchItemNode import ShimmerEffect -struct LocalizationListItemEditing: Equatable { +public struct LocalizationListItemEditing: Equatable { let editable: Bool let editing: Bool let revealed: Bool let reorderable: Bool + + public init(editable: Bool, editing: Bool, revealed: Bool, reorderable: Bool) { + self.editable = editable + self.editing = editing + self.revealed = revealed + self.reorderable = reorderable + } } -class LocalizationListItem: ListViewItem, ItemListItem { +public class LocalizationListItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let id: String let title: String @@ -26,13 +33,14 @@ class LocalizationListItem: ListViewItem, ItemListItem { let activity: Bool let loading: Bool let editing: LocalizationListItemEditing - let sectionId: ItemListSectionId + let enabled: Bool + public let sectionId: ItemListSectionId let alwaysPlain: Bool let action: () -> Void let setItemWithRevealedOptions: (String?, String?) -> Void let removeItem: (String) -> Void - init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { + public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { self.presentationData = presentationData self.id = id self.title = title @@ -41,6 +49,7 @@ class LocalizationListItem: ListViewItem, ItemListItem { self.activity = activity self.loading = loading self.editing = editing + self.enabled = enabled self.sectionId = sectionId self.alwaysPlain = alwaysPlain self.action = action @@ -48,7 +57,7 @@ class LocalizationListItem: ListViewItem, ItemListItem { self.removeItem = removeItem } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = LocalizationListItemNode() var neighbors = itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem) @@ -68,7 +77,7 @@ class LocalizationListItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? LocalizationListItemNode { let makeLayout = nodeValue.asyncLayout() @@ -89,9 +98,9 @@ class LocalizationListItem: ListViewItem, ItemListItem { } } - var selectable: Bool = true + public var selectable: Bool = true - func selected(listView: ListView){ + public func selected(listView: ListView){ listView.clearHighlightAnimated(true) self.action() } diff --git a/submodules/TranslateUI/Sources/PlayPauseIconCompoennt.swift b/submodules/TranslateUI/Sources/PlayPauseIconCompoennt.swift new file mode 100644 index 0000000000..dec0969495 --- /dev/null +++ b/submodules/TranslateUI/Sources/PlayPauseIconCompoennt.swift @@ -0,0 +1,113 @@ +import Foundation +import UIKit +import ComponentFlow +import ManagedAnimationNode + +enum PlayPauseIconNodeState: Equatable { + case play + case pause +} + +private final class PlayPauseIconNode: ManagedAnimationNode { + private let duration: Double = 0.35 + private var iconState: PlayPauseIconNodeState = .play + + init() { + super.init(size: CGSize(width: 40.0, height: 40.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + + func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) { + guard self.iconState != state else { + return + } + + let previousState = self.iconState + self.iconState = state + + switch previousState { + case .pause: + switch state { + case .play: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01)) + } + case .pause: + break + } + case .play: + switch state { + case .pause: + if animated { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration)) + } else { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01)) + } + case .play: + break + } + } + } +} + +final class PlayPauseIconComponent: Component { + let state: PlayPauseIconNodeState + let size: CGSize + + init(state: PlayPauseIconNodeState, size: CGSize) { + self.state = state + self.size = size + } + + static func ==(lhs: PlayPauseIconComponent, rhs: PlayPauseIconComponent) -> Bool { + if lhs.state != rhs.state { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + final class View: UIView { + private var component: PlayPauseIconComponent? + private var animationNode: PlayPauseIconNode + + override init(frame: CGRect) { + self.animationNode = PlayPauseIconNode() + + super.init(frame: frame) + + self.addSubview(self.animationNode.view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PlayPauseIconComponent, availableSize: CGSize, transition: Transition) -> CGSize { + if self.component?.state != component.state { + self.component = component + + self.animationNode.enqueueState(component.state, animated: true) + } + + let animationSize = component.size + let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height)) + self.animationNode.view.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TranslateUI/Sources/Translate.swift b/submodules/TranslateUI/Sources/Translate.swift new file mode 100644 index 0000000000..8fddc7aebc --- /dev/null +++ b/submodules/TranslateUI/Sources/Translate.swift @@ -0,0 +1,271 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import NaturalLanguage +import TelegramCore + +// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker +private final class LinkHelperClass: NSObject { +} + +public var supportedTranslationLanguages = [ + "af", + "sq", + "am", + "ar", + "hy", + "az", + "eu", + "be", + "bn", + "bs", + "bg", + "ca", + "ceb", + "zh-Hans", +// "zh-Hant", +// "zh-CN", "zh" +// "zh-TW" + "co", + "hr", + "cs", + "da", + "nl", + "en", + "eo", + "et", + "fi", + "fr", + "fy", + "gl", + "ka", + "de", + "el", + "gu", + "ht", + "ha", + "haw", + "he", + "hi", + "hmn", + "hu", + "is", + "ig", + "id", + "ga", + "it", + "ja", + "jv", + "kn", + "kk", + "km", + "rw", + "ko", + "ku", + "ky", + "lo", + "lv", + "lt", + "lb", + "mk", + "mg", + "ms", + "ml", + "mt", + "mi", + "mr", + "mn", + "my", + "ne", + "no", + "ny", + "or", + "ps", + "fa", + "pl", + "pt", + "pa", + "ro", + "ru", + "sm", + "gd", + "sr", + "st", + "sn", + "sd", + "si", + "sk", + "sl", + "so", + "es", + "su", + "sw", + "sv", + "tl", + "tg", + "ta", + "tt", + "te", + "th", + "tr", + "tk", + "uk", + "ur", + "ug", + "uz", + "vi", + "cy", + "xh", + "yi", + "yo", + "zu" +] + +public var popularTranslationLanguages = [ + "en", + "ar", + "zh-Hans", +// "zh-Hant", + "fr", + "de", + "it", + "ja", + "ko", + "pt", + "ru", + "es" +] + +@available(iOS 12.0, *) +private let languageRecognizer = NLLanguageRecognizer() + +public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) { + guard showTranslate, text.count > 0 else { + return (false, nil) + } + + if #available(iOS 15.0, *) { + var dontTranslateLanguages: [String] = [] + if let ignoredLanguages = ignoredLanguages { + dontTranslateLanguages = ignoredLanguages + } else { + dontTranslateLanguages = [context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode] + } + + let text = String(text.prefix(64)) + languageRecognizer.processString(text) + let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3) + languageRecognizer.reset() + + let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value }) + if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) { + return (!dontTranslateLanguages.contains(language.key.rawValue), language.key.rawValue) + } else { + return (false, nil) + } + } else { + return (false, nil) + } +} + +public func translateText(context: AccountContext, text: String, fromLang: String? = nil) { + guard !text.isEmpty else { + return + } + if #available(iOS 15.0, *) { + let text = text.unicodeScalars.filter { !$0.properties.isEmojiPresentation }.reduce("") { $0 + String($1) } + + let textView = UITextView() + textView.text = text + textView.isEditable = false + if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { + topController.view.addSubview(textView) + textView.selectAll(nil) + + DispatchQueue.main.async { + textView.removeFromSuperview() + } + } + + let toLang = context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode + let _ = context.engine.messages.translate(text: text, fromLang: fromLang, toLang: toLang).start() + } +} + +public struct TextTranslationResult: Equatable { + let text: String + let detectedLanguage: String? +} + +public enum TextTranslationError { + case generic +} + +private let userAgents: [String] = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", // 13.5% + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", // 6.6% + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0", // 6.4% + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", // 6.2% + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36", // 5.2% + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36" // 4.8% +] + +public func translateText(context: AccountContext, text: String, from: String?, to: String) -> Signal { + return Signal { subscriber in + var uri = "https://translate.goo"; + uri += "gleapis.com/transl"; + uri += "ate_a"; + uri += "/singl"; + uri += "e?client=gtx&sl=" + (from ?? "auto") + "&tl=" + to + "&dt=t" + "&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&kc=7&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&q="; + uri += text.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)! + + var request = URLRequest(url: URL(string: uri)!) + request.httpMethod = "GET" + request.setValue(userAgents[Int.random(in: 0 ..< userAgents.count)], forHTTPHeaderField: "User-Agent") + let session = URLSession.shared + let task = session.dataTask(with: request, completionHandler: { data, response, error in + if let _ = error { + subscriber.putError(.generic) + } else if let data = data { + let json = try? JSONSerialization.jsonObject(with: data, options: []) as? NSArray + if let json = json, json.count > 0 { + let array = json[0] as? NSArray ?? NSArray() + var result: String = "" + for i in 0 ..< array.count { + let blockText = array[i] as? NSArray + if let blockText = blockText, blockText.count > 0 { + let value = blockText[0] as? String + if let value = value, value != "null" { + result += value + } + } + } + + let translationResult = TextTranslationResult(text: result, detectedLanguage: json[2] as? String) + + var fromLang: String? + if let lang = translationResult.detectedLanguage { + fromLang = lang + } else if let lang = from { + fromLang = lang + } + if let fromLang = fromLang { + let _ = context.engine.messages.translate(text: text, fromLang: fromLang, toLang: to).start() + } + + subscriber.putNext(translationResult) + subscriber.putCompletion() + } else { + subscriber.putError(.generic) + } + } + }) + task.resume() + + return ActionDisposable { + task.cancel() + } + } +} diff --git a/submodules/TranslateUI/Sources/TranslateButtonComponent.swift b/submodules/TranslateUI/Sources/TranslateButtonComponent.swift new file mode 100644 index 0000000000..7361ac70d6 --- /dev/null +++ b/submodules/TranslateUI/Sources/TranslateButtonComponent.swift @@ -0,0 +1,183 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import BundleIconComponent + +private final class TranslateButtonContentComponent: CombinedComponent { + let theme: PresentationTheme + let title: String + let icon: String + + init( + theme: PresentationTheme, + title: String, + icon: String + ) { + self.theme = theme + self.title = title + self.icon = icon + } + + static func ==(lhs: TranslateButtonContentComponent, rhs: TranslateButtonContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.icon != rhs.icon { + return false + } + return true + } + + static var body: Body { + let title = Child(Text.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let icon = icon.update( + component: BundleIconComponent( + name: component.icon, + tintColor: component.theme.list.itemPrimaryTextColor + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + + let title = title.update( + component: Text( + text: component.title, + font: Font.regular(17.0), + color: component.theme.list.itemPrimaryTextColor + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let sideInset: CGFloat = 16.0 + + context.add(title + .position(CGPoint(x: sideInset + title.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: context.availableSize.width - sideInset - icon.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +final class TranslateButtonComponent: Component { + private let content: TranslateButtonContentComponent + private let theme: PresentationTheme + private let isEnabled: Bool + private let action: () -> Void + + init( + theme: PresentationTheme, + title: String, + icon: String, + isEnabled: Bool, + action: @escaping () -> Void + ) { + self.content = TranslateButtonContentComponent(theme: theme, title: title, icon: icon) + self.isEnabled = isEnabled + self.theme = theme + self.action = action + } + + static func ==(lhs: TranslateButtonComponent, rhs: TranslateButtonComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.content !== rhs.content { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let backgroundView: UIView + private let centralContentView: ComponentHostView + + private var component: TranslateButtonComponent? + + override init(frame: CGRect) { + self.backgroundView = UIView() + self.backgroundView.isUserInteractionEnabled = false + + self.centralContentView = ComponentHostView() + self.centralContentView.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + + self.addSubview(self.backgroundView) + self.addSubview(self.centralContentView) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self, let component = strongSelf.component { + if highlighted { + strongSelf.backgroundView.backgroundColor = component.theme.list.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor + }) + } + } + } + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func pressed() { + if let component = self.component { + component.action() + } + } + + public func update(component: TranslateButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + self.backgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor + self.backgroundView.layer.cornerRadius = 10.0 + + let _ = self.centralContentView.update( + transition: transition, + component: AnyComponent(component.content), + environment: {}, + containerSize: availableSize + ) + transition.setFrame(view: self.centralContentView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) + + self.centralContentView.alpha = component.isEnabled ? 1.0 : 0.4 + self.isUserInteractionEnabled = component.isEnabled + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift new file mode 100644 index 0000000000..1bf9889561 --- /dev/null +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -0,0 +1,1109 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import Speak +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BundleIconComponent +import UndoUI + +private func generateExpandBackground(size: CGSize) -> UIImage { + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let color = UIColor.white + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [color.withAlphaComponent(0.0).cgColor, color.cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 40.0, y: size.height), options: CGGradientDrawingOptions()) + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(x: 40.0, y: 0.0), size: CGSize(width: size.width - 40.0, height: size.height))) + })! +} + +private final class TranslateScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let text: String + let fromLanguage: String? + let toLanguage: String + let copyTranslation: (String) -> Void + let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void + let expand: () -> Void + + init(context: AccountContext, text: String, fromLanguage: String?, toLanguage: String, copyTranslation: @escaping (String) -> Void, changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void, expand: @escaping () -> Void) { + self.context = context + self.text = text + self.fromLanguage = fromLanguage + self.toLanguage = toLanguage + self.copyTranslation = copyTranslation + self.changeLanguage = changeLanguage + self.expand = expand + } + + static func ==(lhs: TranslateScreenComponent, rhs: TranslateScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.fromLanguage != rhs.fromLanguage { + return false + } + if lhs.toLanguage != rhs.toLanguage { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + var fromLanguage: String? + let text: String + var textExpanded: Bool = false + + var toLanguage: String + var translatedText: String? + + private let expand: () -> Void + + private var translationDisposable = MetaDisposable() + + fileprivate var isSpeakingOriginalText: Bool = false + fileprivate var isSpeakingTranslatedText: Bool = false + private var speechHolder: SpeechSynthesizerHolder? + + fileprivate var moreBackgroundImage: (CGSize, UIImage)? + + init(context: AccountContext, fromLanguage: String?, text: String, toLanguage: String, expand: @escaping () -> Void) { + self.context = context + self.text = text + self.fromLanguage = fromLanguage + self.toLanguage = toLanguage + self.expand = expand + + super.init() + + self.translationDisposable.set((translateText(context: context, text: text, from: fromLanguage, to: toLanguage) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.translatedText = result.text + if strongSelf.fromLanguage == nil { + strongSelf.fromLanguage = result.detectedLanguage + } + strongSelf.updated(transition: .immediate) + }, error: { error in + + })) + } + + deinit { + self.translationDisposable.dispose() + } + + func changeLanguage(fromLanguage: String, toLanguage: String) { + guard self.fromLanguage != fromLanguage || self.toLanguage != toLanguage else { + return + } + self.fromLanguage = fromLanguage + self.toLanguage = toLanguage + self.translatedText = nil + self.updated(transition: .immediate) + + self.translationDisposable.set((translateText(context: self.context, text: text, from: fromLanguage, to: toLanguage) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.translatedText = result.text + if strongSelf.fromLanguage == nil { + strongSelf.fromLanguage = result.detectedLanguage + } + strongSelf.updated(transition: .immediate) + }, error: { error in + + })) + } + + func expandText() { + self.textExpanded = true + self.updated(transition: .immediate) + + self.expand() + } + + func speakOriginalText() { + if let speechHolder = self.speechHolder { + self.speechHolder = nil + speechHolder.stop() + } + + if self.isSpeakingOriginalText { + self.isSpeakingOriginalText = false + } else { + self.isSpeakingTranslatedText = false + + self.isSpeakingOriginalText = true + self.speechHolder = speakText(self.text) + self.speechHolder?.completion = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.isSpeakingOriginalText = false + strongSelf.updated(transition: .immediate) + } + } + self.updated(transition: .immediate) + } + + func speakTranslatedText() { + guard let translatedText = self.translatedText else { + return + } + + if let speechHolder = self.speechHolder { + self.speechHolder = nil + speechHolder.stop() + } + + if self.isSpeakingTranslatedText { + self.isSpeakingTranslatedText = false + } else { + self.isSpeakingOriginalText = false + + self.isSpeakingTranslatedText = true + self.speechHolder = speakText(translatedText) + self.speechHolder?.completion = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.isSpeakingTranslatedText = false + strongSelf.updated(transition: .immediate) + } + } + self.updated(transition: .immediate) + } + } + + func makeState() -> State { + return State(context: self.context, fromLanguage: self.fromLanguage, text: self.text, toLanguage: self.toLanguage, expand: self.expand) + } + + static var body: Body { + let textBackground = Child(RoundedRectangle.self) + + let originalTitle = Child(MultilineTextComponent.self) + let originalText = Child(MultilineTextComponent.self) + + let originalMoreBackground = Child(Image.self) + let originalMoreButton = Child(Button.self) + + let originalSpeakButton = Child(Button.self) + + let translationTitle = Child(MultilineTextComponent.self) + let translationText = Child(MultilineTextComponent.self) + let translationPlaceholder = Child(RoundedRectangle.self) + let translationSpeakButton = Child(Button.self) + + let copyButton = Child(TranslateButtonComponent.self) + let changeLanguageButton = Child(TranslateButtonComponent.self) + + let textStripe = Child(Rectangle.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let state = context.state + let theme = environment.theme + let strings = environment.strings + + let topInset: CGFloat = environment.navigationHeight + 22.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textTopInset: CGFloat = 16.0 + let textSideInset: CGFloat = 16.0 + let textSpacing: CGFloat = 5.0 + let itemSpacing: CGFloat = 16.0 + let itemHeight: CGFloat = 44.0 + + let locale = Locale(identifier: environment.strings.baseLanguageCode) + let fromLanguage: String + if let languageCode = state.fromLanguage { + fromLanguage = locale.localizedString(forLanguageCode: languageCode) ?? "" + } else { + fromLanguage = "" + } + let originalTitle = originalTitle.update( + component: MultilineTextComponent( + text: NSAttributedString(string: fromLanguage, font: Font.medium(13.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural), + horizontalAlignment: .natural, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let originalText = originalText.update( + component: MultilineTextComponent( + text: NSAttributedString(string: state.text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural), + horizontalAlignment: .natural, + maximumNumberOfLines: state.textExpanded ? 0 : 1, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0 - (state.textExpanded ? 30.0 : 0.0), height: context.availableSize.height), + transition: .immediate + ) + + let toLanguage = locale.localizedString(forLanguageCode: state.toLanguage) ?? "" + let translationTitle = translationTitle.update( + component: MultilineTextComponent( + text: NSAttributedString(string: toLanguage, font: Font.medium(13.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural), + horizontalAlignment: .natural, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let translationTextHeight: CGFloat + + var maybeTranslationText: _UpdatedChildComponent? = nil + var maybeTranslationPlaceholder: _UpdatedChildComponent? = nil + if let translatedText = state.translatedText { + maybeTranslationText = translationText.update( + component: MultilineTextComponent( + text: NSAttributedString(string: translatedText, font: Font.medium(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0 - 30.0, height: context.availableSize.height), + transition: .immediate + ) + translationTextHeight = maybeTranslationText?.size.height ?? 0.0 + } else { + maybeTranslationPlaceholder = translationPlaceholder.update( + component: RoundedRectangle(color: theme.list.itemAccentColor.withAlphaComponent(0.17), cornerRadius: 6.0), + availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0 - 42.0, height: 12.0), + transition: .immediate + ) + translationTextHeight = 22.0 + } + + let textBackgroundOrigin = CGPoint(x: sideInset, y: topInset) + + let textStripe = textStripe.update( + component: Rectangle(color: theme.list.itemPlainSeparatorColor), + availableSize: CGSize(width: context.availableSize.width - (sideInset + textSideInset) * 2.0, height: UIScreenPixel), + transition: .immediate + ) + + let textBackgroundSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing) + + let textBackground = textBackground.update( + component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0), + availableSize: textBackgroundSize, + transition: context.transition + ) + + context.add(textBackground + .position(CGPoint(x: textBackgroundOrigin.x + textBackgroundSize.width / 2.0, y: topInset + textBackgroundSize.height / 2.0)) + ) + + context.add(textStripe + .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + textStripe.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing)) + ) + + context.add(originalTitle + .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + originalTitle.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height / 2.0)) + ) + context.add(originalText + .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + originalText.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height / 2.0)) + ) + + if state.textExpanded { + let originalSpeakButton = originalSpeakButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( + fillColor: theme.list.itemPrimaryTextColor, + size: CGSize(width: 22.0, height: 22.0) + ))), + AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent( + state: state.isSpeakingOriginalText ? .pause : .play, + size: CGSize(width: 18.0, height: 18.0) + ))), + ])), + action: { [weak state] in + guard let state = state else { + return + } + state.speakOriginalText() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 22.0, height: 22.0), + transition: .immediate + ) + + context.add(originalSpeakButton + .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalSpeakButton.size.width / 2.0 - 3.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height - originalSpeakButton.size.height / 2.0 - 2.0)) + ) + } else { + let originalMoreButton = originalMoreButton.update( + component: Button( + content: AnyComponent(Text(text: strings.PeerInfo_BioExpand, font: Font.regular(17.0), color: theme.list.itemAccentColor)), + action: { [weak state] in + guard let state = state else { + return + } + state.expandText() + } + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let originalMoreBackgroundSize = CGSize(width: originalMoreButton.size.width + 50.0, height: originalMoreButton.size.height) + let originalMoreBackgroundImage: UIImage + if let (size, image) = state.moreBackgroundImage, size == originalMoreBackgroundSize { + originalMoreBackgroundImage = image + } else { + originalMoreBackgroundImage = generateExpandBackground(size: originalMoreBackgroundSize) + state.moreBackgroundImage = (originalMoreBackgroundSize, originalMoreBackgroundImage) + } + let originalMoreBackground = originalMoreBackground.update( + component: Image(image: originalMoreBackgroundImage, tintColor: theme.list.itemBlocksBackgroundColor), + availableSize: originalMoreBackgroundSize, + transition: .immediate + ) + + context.add(originalMoreBackground + .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalMoreBackground.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalMoreBackground.size.height / 2.0 - 1.0)) + ) + + context.add(originalMoreButton + .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalMoreButton.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height / 2.0 - 1.0)) + ) + } + + context.add(translationTitle + .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationTitle.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height / 2.0)) + ) + + if let translationText = maybeTranslationText { + context.add(translationText + .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationText.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationText.size.height / 2.0)) + ) + + let translationSpeakButton = translationSpeakButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( + fillColor: theme.list.itemAccentColor, + size: CGSize(width: 22.0, height: 22.0) + ))), + AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent( + state: state.isSpeakingTranslatedText ? .pause : .play, + size: CGSize(width: 18.0, height: 18.0) + ))), + ])), + action: { [weak state] in + guard let state = state else { + return + } + state.speakTranslatedText() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 22.0, height: 22.0), + transition: .immediate + ) + + context.add(translationSpeakButton + .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - translationSpeakButton.size.width / 2.0 - 3.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationText.size.height - translationSpeakButton.size.height / 2.0 - 2.0)) + ) + } else if let translationPlaceholder = maybeTranslationPlaceholder { + context.add(translationPlaceholder + .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationPlaceholder.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationPlaceholder.size.height / 2.0 + 4.0)) + ) + } + + let component = context.component + let copyButton = copyButton.update( + component: TranslateButtonComponent( + theme: theme, + title: strings.Translate_CopyTranslation, + icon: "Chat/Context Menu/Copy", + isEnabled: state.translatedText != nil, + action: { [weak component] in + component?.copyTranslation(state.translatedText ?? "") + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: itemHeight), + transition: context.transition + ) + + let changeLanguageButton = changeLanguageButton.update( + component: TranslateButtonComponent( + theme: theme, + title: strings.Translate_ChangeLanguage, + icon: "Chat/Context Menu/Translate", + isEnabled: true, + action: { [weak component] in + component?.changeLanguage(state.fromLanguage ?? "", state.toLanguage, { fromLang, toLang in + state.changeLanguage(fromLanguage: fromLang, toLanguage: toLang) + }) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: itemHeight), + transition: context.transition + ) + + let buttonsSpacing: CGFloat = 24.0 + let smallSectionSpacing: CGFloat = 8.0 + + context.add(copyButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing + buttonsSpacing + copyButton.size.height / 2.0)) + ) + + context.add(changeLanguageButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing + buttonsSpacing + copyButton.size.height + smallSectionSpacing + changeLanguageButton.size.height / 2.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing + buttonsSpacing + copyButton.size.height + smallSectionSpacing + changeLanguageButton.size.height + environment.safeInsets.bottom + 44.0) + + return contentSize + } + } +} + +public class TranslateScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: TranslateScreen? + + private let component: AnyComponent + private let theme: PresentationTheme? + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + let scrollView: UIScrollView + let hostView: ComponentHostView + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? + + private var currentIsVisible: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + fileprivate var temporaryDismiss = false + + init(context: AccountContext, controller: TranslateScreen, component: AnyComponent, theme: PresentationTheme?) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.controller = controller + + self.component = component + self.theme = theme + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.scrollView = UIScrollView() + self.hostView = ComponentHostView() + + super.init() + + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.scrollView) + self.scrollView.addSubview(self.hostView) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let (layout, _) = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = self.scrollView.contentOffset.y + self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + if !self.temporaryDismiss { + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + self.currentLayout = (layout, navigationHeight) + + if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { + self.containerView.addSubview(navigationBar.view) + } + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + var containerTopInset: CGFloat = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: 0.0, + navigationHeight: navigationHeight, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + isVisible: self.currentIsVisible, + theme: self.theme ?? self.presentationData.theme, + strings: self.presentationData.strings, + controller: { [weak self] in + return self?.controller + } + ) + let contentSize = self.hostView.update( + transition: transition, + component: self.component, + environment: { + environment + }, + forceUpdate: true, + containerSize: CGSize(width: clipFrame.size.width, height: 10000.0) + ) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + + self.scrollView.contentSize = contentSize + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + guard let (layout, _) = self.currentLayout else{ + return 210.0 + } + if case .compact = layout.metrics.widthClass { + var factor: CGFloat = 0.2488 + if layout.size.width <= 320.0 { + factor = 0.15 + } + return floor(max(layout.size.width, layout.size.height) * factor) + } else { + return 210.0 + } + } + + private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { + if let view = view { + if let view = view as? UIScrollView { + return (view, nil) + } + if let node = view.asyncdisplaykit_node as? ListView { + return (node.scroller, node) + } + return findScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollViewAndListNode = self.findScrollView(view: currentHitView) + if scrollViewAndListNode?.0.frame.height == self.frame.width { + scrollViewAndListNode = nil + } + let scrollView = scrollViewAndListNode?.0 + let listNode = scrollViewAndListNode?.1 + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView, listNode) + case .changed: + guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if case let .known(value) = visibleContentOffset, value <= epsilon { + if let scrollView = scrollView { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) + } + } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + self.panGestureArguments = (topInset, translation, scrollView, listNode) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if case let .known(value) = visibleContentOffset, value > 0.1 { + velocity = CGPoint() + } else if case .unknown = visibleContentOffset { + velocity = CGPoint() + } else if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if (velocity.y < -300.0 || offset < topInset / 2.0) { + if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { + DispatchQueue.main.async { + listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.isExpanded = isExpanded + + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let theme: PresentationTheme? + private let component: AnyComponent + private var isInitiallyExpanded = false + + private var currentLayout: ContainerViewLayout? + + public var pushController: (ViewController) -> Void = { _ in } + public var presentController: (ViewController) -> Void = { _ in } + + public convenience init(context: AccountContext, text: String, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var toLanguage = toLanguage ?? presentationData.strings.baseLanguageCode + + if toLanguage == fromLanguage { + toLanguage = "en" + } + + var copyTranslationImpl: ((String) -> Void)? + var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)? + var expandImpl: (() -> Void)? + self.init(context: context, component: TranslateScreenComponent(context: context, text: text, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: { text in + copyTranslationImpl?(text) + }, changeLanguage: { fromLang, toLang, completion in + changeLanguageImpl?(fromLang, toLang, completion) + }, expand: { + expandImpl?() + })) + + self.isInitiallyExpanded = isExpanded + + self.title = presentationData.strings.Translate_Title + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + copyTranslationImpl = { [weak self] text in + let content = UndoOverlayContent.copy(text: presentationData.strings.Conversation_TextCopied) + self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + self?.dismiss(animated: true, completion: nil) + } + + changeLanguageImpl = { [weak self] fromLang, toLang, completion in + let pushController = self?.pushController + let presentController = self?.presentController + let controller = languageSelectionController(context: context, fromLanguage: fromLang, toLanguage: toLang, completion: { fromLang, toLang in + let controller = TranslateScreen(context: context, text: text, fromLanguage: fromLang, toLanguage: toLang, isExpanded: true) + controller.pushController = pushController ?? { _ in } + controller.presentController = presentController ?? { _ in } + presentController?(controller) + }) + + self?.node.temporaryDismiss = true + self?.dismiss(animated: true, completion: nil) + + pushController?(controller) + } + + expandImpl = { [weak self] in + self?.node.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) + if let currentLayout = self?.currentLayout { + self?.containerLayoutUpdated(currentLayout, transition: .animated(duration: 0.4, curve: .spring)) + } + } + } + + private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.context = context + self.component = AnyComponent(component) + self.theme = nil + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 })) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss(animated: true, completion: nil) + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) + if self.isInitiallyExpanded { + (self.displayNode as! Node).update(isExpanded: true, transition: .immediate) + } + self.displayNodeDidLoad() + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var navigationLayout = self.navigationLayout(layout: layout) + var navigationFrame = navigationLayout.navigationFrame + + var layout = layout + if case .regular = layout.metrics.widthClass { + let verticalInset: CGFloat = 44.0 + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + navigationFrame.size.width = clipFrame.width + layout.size = clipFrame.size + } + + navigationFrame.size.height = 56.0 + navigationLayout.navigationFrame = navigationFrame + navigationLayout.defaultContentHeight = 56.0 + + layout.statusBarHeight = nil + + self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, transition: transition) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat = 56.0 + + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } +} diff --git a/versions.json b/versions.json index fb53b60213..fbf91da0d3 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "8.6", + "app": "8.6.1", "bazel": "5.0.0", "xcode": "13.2.1" }