Cherry-pick various fixes

This commit is contained in:
Ilya Laktyushin 2022-03-22 18:22:49 +04:00
parent 702f254783
commit 000b1c7339
40 changed files with 2213 additions and 180 deletions

View File

@ -7365,3 +7365,38 @@ Sorry for the inconvenience.";
"CreateExternalStream.StreamKey" = "stream key"; "CreateExternalStream.StreamKey" = "stream key";
"CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below."; "CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below.";
"CreateExternalStream.StartStreaming" = "Start Streaming"; "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.";

View File

@ -1143,7 +1143,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
return (current, inputMode) return (current, inputMode)
} }
speakText(text) let _ = speakText(text)
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
UIMenuController.shared.hideMenu() UIMenuController.shared.hideMenu()

View File

@ -255,7 +255,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let topInset: CGFloat = edgeTopInset let topInset: CGFloat = edgeTopInset
var dismissing = false 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?() self.interactivelyDismissed?()
dismissing = true dismissing = true
} else if self.isExpanded { } else if self.isExpanded {

View File

@ -279,6 +279,9 @@ public class AttachmentController: ViewController {
} }
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
guard !self.isDismissing else {
return
}
if case .ended = recognizer.state { if case .ended = recognizer.state {
if let controller = self.currentControllers.last { if let controller = self.currentControllers.last {
controller.requestDismiss(completion: { [weak self] in controller.requestDismiss(completion: { [weak self] in
@ -436,9 +439,9 @@ public class AttachmentController: ViewController {
self.animating = true self.animating = true
if case .regular = layout.metrics.widthClass { if case .regular = layout.metrics.widthClass {
self.layer.allowsGroupOpacity = true self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in 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) let _ = self?.container.dismiss(transition: .immediate, completion: completion)
self.animating = false self?.animating = false
}) })
} else { } else {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
@ -515,7 +518,6 @@ public class AttachmentController: ViewController {
self.wrapperNode.view.mask = nil self.wrapperNode.view.mask = nil
} }
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting) 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) let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, transition: transition)
var panelTransition = transition var panelTransition = transition
@ -583,6 +585,10 @@ public class AttachmentController: ViewController {
} }
} }
deinit {
print()
}
public required init(coder aDecoder: NSCoder) { public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -596,11 +602,15 @@ public class AttachmentController: ViewController {
self.displayNodeDidLoad() self.displayNodeDidLoad()
} }
public func _dismiss() {
super.dismiss(animated: false, completion: {})
}
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.view.endEditing(true) self.view.endEditing(true)
if flag { if flag {
self.node.animateOut(completion: { self.node.animateOut(completion: { [weak self] in
super.dismiss(animated: false, completion: {}) self?._dismiss()
completion?() completion?()
}) })
} else { } else {

View File

@ -275,7 +275,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
} }
} }
strongSelf.editableControlNode = editableControlNode strongSelf.editableControlNode = editableControlNode
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode) strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.containerNode)
editableControlNode.frame = editableControlFrame editableControlNode.frame = editableControlFrame
transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY))
editableControlNode.alpha = 0.0 editableControlNode.alpha = 0.0

View File

@ -5,6 +5,7 @@ public final class Button: Component {
public let content: AnyComponent<Empty> public let content: AnyComponent<Empty>
public let minSize: CGSize? public let minSize: CGSize?
public let tag: AnyObject? public let tag: AnyObject?
public let automaticHighlight: Bool
public let action: () -> Void public let action: () -> Void
convenience public init( convenience public init(
@ -15,6 +16,7 @@ public final class Button: Component {
content: content, content: content,
minSize: nil, minSize: nil,
tag: nil, tag: nil,
automaticHighlight: true,
action: action action: action
) )
} }
@ -23,11 +25,13 @@ public final class Button: Component {
content: AnyComponent<Empty>, content: AnyComponent<Empty>,
minSize: CGSize?, minSize: CGSize?,
tag: AnyObject? = nil, tag: AnyObject? = nil,
automaticHighlight: Bool = true,
action: @escaping () -> Void action: @escaping () -> Void
) { ) {
self.content = content self.content = content
self.minSize = nil self.minSize = nil
self.tag = tag self.tag = tag
self.automaticHighlight = automaticHighlight
self.action = action self.action = action
} }
@ -36,6 +40,7 @@ public final class Button: Component {
content: self.content, content: self.content,
minSize: minSize, minSize: minSize,
tag: self.tag, tag: self.tag,
automaticHighlight: self.automaticHighlight,
action: self.action action: self.action
) )
} }
@ -45,6 +50,7 @@ public final class Button: Component {
content: self.content, content: self.content,
minSize: self.minSize, minSize: self.minSize,
tag: tag, tag: tag,
automaticHighlight: self.automaticHighlight,
action: self.action action: self.action
) )
} }
@ -59,6 +65,9 @@ public final class Button: Component {
if lhs.tag !== rhs.tag { if lhs.tag !== rhs.tag {
return false return false
} }
if lhs.automaticHighlight != rhs.automaticHighlight {
return false
}
return true return true
} }
@ -68,6 +77,9 @@ public final class Button: Component {
private var component: Button? private var component: Button?
private var currentIsHighlighted: Bool = false { private var currentIsHighlighted: Bool = false {
didSet { didSet {
guard let component = self.component, component.automaticHighlight else {
return
}
if self.currentIsHighlighted != oldValue { if self.currentIsHighlighted != oldValue {
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
} }

View File

@ -2,26 +2,31 @@ import Foundation
import UIKit import UIKit
public final class Circle: Component { 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 size: CGSize
public let width: CGFloat
public init(color: UIColor, size: CGSize, width: CGFloat) { public init(fillColor: UIColor = .clear, strokeColor: UIColor = .clear, strokeWidth: CGFloat = 0.0, size: CGSize) {
self.color = color self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
self.size = size self.size = size
self.width = width
} }
public static func ==(lhs: Circle, rhs: Circle) -> Bool { 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 return false
} }
if lhs.size != rhs.size { if lhs.size != rhs.size {
return false return false
} }
if lhs.width != rhs.width {
return false
}
return true return true
} }
@ -38,9 +43,13 @@ public final class Circle: Component {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0) UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
if let context = UIGraphicsGetCurrentContext() { if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(component.color.cgColor) context.setFillColor(component.fillColor.cgColor)
context.setLineWidth(component.width) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: component.width / 2.0, dy: component.width / 2.0)) 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() self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext() UIGraphicsEndImageContext()

View File

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

View File

@ -32,8 +32,8 @@ public final class ComponentHostView<EnvironmentType: Equatable>: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, containerSize: CGSize) -> CGSize { public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, forceUpdate: Bool = false, containerSize: CGSize) -> CGSize {
let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: containerSize) let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: forceUpdate, containerSize: containerSize)
self.currentSize = size self.currentSize = size
return size return size
} }

View File

@ -10,6 +10,7 @@ public final class MultilineTextComponent: Component {
public var truncationType: CTLineTruncationType public var truncationType: CTLineTruncationType
public var maximumNumberOfLines: Int public var maximumNumberOfLines: Int
public var lineSpacing: CGFloat public var lineSpacing: CGFloat
public var cutout: TextNodeCutout?
public var insets: UIEdgeInsets public var insets: UIEdgeInsets
public var textShadowColor: UIColor? public var textShadowColor: UIColor?
public var textStroke: (UIColor, CGFloat)? public var textStroke: (UIColor, CGFloat)?
@ -21,6 +22,7 @@ public final class MultilineTextComponent: Component {
truncationType: CTLineTruncationType = .end, truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1, maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0, lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(), insets: UIEdgeInsets = UIEdgeInsets(),
textShadowColor: UIColor? = nil, textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil textStroke: (UIColor, CGFloat)? = nil
@ -31,6 +33,7 @@ public final class MultilineTextComponent: Component {
self.truncationType = truncationType self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets self.insets = insets
self.textShadowColor = textShadowColor self.textShadowColor = textShadowColor
self.textStroke = textStroke self.textStroke = textStroke
@ -55,6 +58,9 @@ public final class MultilineTextComponent: Component {
if lhs.lineSpacing != rhs.lineSpacing { if lhs.lineSpacing != rhs.lineSpacing {
return false return false
} }
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets { if lhs.insets != rhs.insets {
return false return false
} }
@ -93,7 +99,7 @@ public final class MultilineTextComponent: Component {
alignment: component.horizontalAlignment, alignment: component.horizontalAlignment,
verticalAlignment: component.verticalAlignment, verticalAlignment: component.verticalAlignment,
lineSpacing: component.lineSpacing, lineSpacing: component.lineSpacing,
cutout: nil, cutout: component.cutout,
insets: component.insets, insets: component.insets,
textShadowColor: component.textShadowColor, textShadowColor: component.textShadowColor,
textStroke: component.textStroke, textStroke: component.textStroke,

View File

@ -21,6 +21,15 @@ public extension Transition.Animation.Curve {
self = .spring self = .spring
} }
} }
var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve {
switch self {
case .easeInOut:
return .easeInOut
case .spring:
return .spring
}
}
} }
public extension Transition { public extension Transition {
@ -32,6 +41,15 @@ public extension Transition {
self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) 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 { open class ViewControllerComponentContainer: ViewController {
@ -125,7 +143,7 @@ open class ViewControllerComponentContainer: ViewController {
let environment = ViewControllerComponentContainer.Environment( let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0, statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: navigationHeight, 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, isVisible: self.currentIsVisible,
theme: self.presentationData.theme, theme: self.presentationData.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,

View File

@ -848,6 +848,11 @@ open class NavigationController: UINavigationController, ContainableController,
} }
} }
if self._keepModalDismissProgress {
modalStyleOverlayTransitionFactor = 0.0
self._keepModalDismissProgress = false
}
topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor) topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor)
switch layout.metrics.widthClass { switch layout.metrics.widthClass {
@ -1358,6 +1363,7 @@ open class NavigationController: UINavigationController, ContainableController,
self._viewControllersPromise.set(self.viewControllers) self._viewControllersPromise.set(self.viewControllers)
} }
public var _keepModalDismissProgress = false
public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) { public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) {
let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1385,6 +1391,7 @@ open class NavigationController: UINavigationController, ContainableController,
} }
} }
} }
strongSelf.updateContainersNonReentrant(transition: .immediate) strongSelf.updateContainersNonReentrant(transition: .immediate)
}, statusBarUpdated: { [weak self] transition in }, statusBarUpdated: { [weak self] transition in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -39,7 +39,7 @@ swift_library(
"//submodules/Speak:Speak", "//submodules/Speak:Speak",
"//submodules/UndoUI:UndoUI", "//submodules/UndoUI:UndoUI",
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/Translate:Translate", "//submodules/TranslateUI:TranslateUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -16,7 +16,7 @@ import PresentationDataUtils
import ImageContentAnalysis import ImageContentAnalysis
import TextSelectionNode import TextSelectionNode
import Speak import Speak
import Translate import TranslateUI
import ShareController import ShareController
import UndoUI import UndoUI
@ -352,9 +352,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
window.rootViewController?.present(controller, animated: true) window.rootViewController?.present(controller, animated: true)
} }
case .speak: case .speak:
speakText(string) let _ = speakText(string)
case .translate: 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 recognizedContentNode.barcodeAction = { [weak self] payload, rect in

View File

@ -25,7 +25,7 @@ swift_library(
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/LocationResources:LocationResources", "//submodules/LocationResources:LocationResources",
"//submodules/UndoUI:UndoUI", "//submodules/UndoUI:UndoUI",
"//submodules/Translate:Translate", "//submodules/TranslateUI:TranslateUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -16,7 +16,7 @@ import OpenInExternalAppUI
import LocationUI import LocationUI
import UndoUI import UndoUI
import ContextUI import ContextUI
import Translate import TranslateUI
final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
private weak var controller: InstantPageController? 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) let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
if canTranslate { if canTranslate {
actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in
translateText(context: context, text: text, fromLang: language) let controller = TranslateScreen(context: context, text: text, fromLanguage: language)
self?.present(controller, nil)
})) }))
} }

View File

@ -21,8 +21,6 @@ import SparseItemGrid
import UndoUI import UndoUI
import PresentationDataUtils import PresentationDataUtils
let overflowInset: CGFloat = 0.0
final class MediaPickerInteraction { final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
@ -248,7 +246,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.gridNode.visibleContentOffsetChanged = { [weak self] _ in self.gridNode.visibleContentOffsetChanged = { [weak self] _ in
self?.updateNavigation(transition: .immediate) self?.updateNavigation(transition: .immediate)
self?.updateScrollingArea()
} }
self.hiddenMediaDisposable = (self.hiddenMediaId.get() self.hiddenMediaDisposable = (self.hiddenMediaId.get()
@ -325,7 +322,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if self.controller?.collection != nil { if self.controller?.collection != nil {
self.gridNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in 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 { 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 { guard let (layout, _) = self.validLayout else {
return return
} }
var tag: Int32? var tag: Int32?
self.gridNode.forEachItemNode { itemNode in self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode { if let itemNode = itemNode as? MediaPickerGridItemNode {
tag = itemNode.tag tag = itemNode.tag
} }
} }
let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) } let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) }
if self.currentScrollingTag != tag { if self.currentScrollingTag != tag {
self.currentScrollingTag = tag self.currentScrollingTag = tag
@ -421,7 +418,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.scrollingArea.feedbackTap() self.scrollingArea.feedbackTap()
} }
} }
self.scrollingArea.update( self.scrollingArea.update(
containerSize: layout.size, containerSize: layout.size,
containerInsets: self.gridNode.gridLayout.insets, containerInsets: self.gridNode.gridLayout.insets,
@ -848,7 +845,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
insets.top += navigationBarHeight insets.top += navigationBarHeight
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) 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 let itemsPerRow: Int
if case .compact = layout.metrics.widthClass { if case .compact = layout.metrics.widthClass {
@ -955,7 +952,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
transition.updateFrame(node: self.backgroundNode, frame: innerBounds) transition.updateFrame(node: self.backgroundNode, frame: innerBounds)
self.backgroundNode.update(size: bounds.size, transition: transition) 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 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 { guard let strongSelf = self else {
@ -1060,6 +1057,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
} }
private let groupedPromise = ValuePromise<Bool>(true) private let groupedPromise = ValuePromise<Bool>(true)
private var isDismissing = false
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?, collection: PHAssetCollection? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?, collection: PHAssetCollection? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil) {
self.context = context self.context = context
@ -1160,7 +1159,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
} }
} }
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in }, 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 { if let currentItem = currentItem {
selectionState.setItem(currentItem, selected: true) selectionState.setItem(currentItem, selected: true)
} }
@ -1285,11 +1285,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
public func requestDismiss(completion: @escaping () -> Void) { public func requestDismiss(completion: @escaping () -> Void) {
if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 { 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: { 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: { }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, action: {
completion() completion()
})]) })])
controller.dismissed = { [weak self] in
self?.isDismissing = false
}
self.present(controller, in: .window(.root)) self.present(controller, in: .window(.root))
} else { } else {
completion() completion()

View File

@ -93,7 +93,7 @@ swift_library(
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebPBinding:WebPBinding", "//submodules/WebPBinding:WebPBinding",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent", "//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/Translate:Translate", "//submodules/TranslateUI:TranslateUI",
"//submodules/QrCodeUI:QrCodeUI", "//submodules/QrCodeUI:QrCodeUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",

View File

@ -15,7 +15,7 @@ import SearchBarNode
import SearchUI import SearchUI
import UndoUI import UndoUI
import TelegramUIPreferences import TelegramUIPreferences
import Translate import TranslateUI
private enum LanguageListSection: ItemListSectionId { private enum LanguageListSection: ItemListSectionId {
case translate case translate
@ -470,7 +470,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value)) 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)) entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo))
} else { } 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 { if let strongSelf = self {
strongSelf.push(translationSettingsController(context: strongSelf.context)) 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) strongSelf.enqueueTransition(transition)
}) })
self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start()

View File

@ -10,7 +10,7 @@ import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import TelegramStringFormatting import TelegramStringFormatting
import AccountContext import AccountContext
import Translate import TranslateUI
private final class TranslationSettingsControllerArguments { private final class TranslationSettingsControllerArguments {
let context: AccountContext let context: AccountContext
@ -112,10 +112,10 @@ public func translationSettingsController(context: AccountContext) -> ViewContro
}).start() }).start()
}) })
let enLocale = Locale(identifier: "en") let enLocale = Locale(identifier: "en")
var languages: [(String, String, String)] = [] var languages: [(String, String, String)] = []
for code in supportedTranslationLanguages { var addedLanguages = Set<String>()
for code in popularTranslationLanguages {
if let title = enLocale.localizedString(forLanguageCode: code) { if let title = enLocale.localizedString(forLanguageCode: code) {
let languageLocale = Locale(identifier: code) let languageLocale = Locale(identifier: code)
let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title
@ -125,6 +125,20 @@ public func translationSettingsController(context: AccountContext) -> ViewContro
} else { } else {
languages.append(value) 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)
}
} }
} }

View File

@ -5,9 +5,31 @@ import AVFoundation
private final class LinkHelperClass: NSObject { 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 { guard !text.isEmpty else {
return return nil
} }
let speechSynthesizer = AVSpeechSynthesizer() let speechSynthesizer = AVSpeechSynthesizer()
let utterance = AVSpeechUtterance(string: text) let utterance = AVSpeechUtterance(string: text)
@ -15,4 +37,6 @@ public func speakText(_ text: String) {
utterance.voice = AVSpeechSynthesisVoice(language: language) utterance.voice = AVSpeechSynthesisVoice(language: language)
} }
speechSynthesizer.speak(utterance) speechSynthesizer.speak(utterance)
return SpeechSynthesizerHolder(speechSynthesizer: speechSynthesizer)
} }

View File

@ -641,9 +641,9 @@ public final class MediaStreamComponent: CombinedComponent {
navigationRightItems.append(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button( navigationRightItems.append(AnyComponentWithIdentity(id: "more", component: AnyComponent(Button(
content: AnyComponent(ZStack([ content: AnyComponent(ZStack([
AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle(
color: .white, strokeColor: .white,
size: CGSize(width: 22.0, height: 22.0), strokeWidth: 1.5,
width: 1.5 size: CGSize(width: 22.0, height: 22.0)
))), ))),
AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent( AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.Animation( animation: LottieAnimationComponent.Animation(

View File

@ -537,7 +537,9 @@ private final class CallSessionManagerContext {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 self.contextIdByStableId[stableId] = internalId

View File

@ -30,15 +30,16 @@ private func generateInstantVideoBackground(fillColor: UIColor, strokeColor: UIC
private func generateActionPhotoBackground(fillColor: UIColor, strokeColor: UIColor) -> UIImage? { private func generateActionPhotoBackground(fillColor: UIColor, strokeColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 214.0, height: 214.0), rotatedContext: { size, context in return generateImage(CGSize(width: 214.0, height: 214.0), rotatedContext: { size, context in
let lineWidth: CGFloat = 0.5 let lineWidth: CGFloat = 0.5
let cornerRadius: CGFloat = 16.0
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(strokeColor.cgColor) 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.addPath(strokePath.cgPath)
context.fillPath() context.fillPath()
context.setFillColor(fillColor.cgColor) 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.addPath(fillPath.cgPath)
context.fillPath() context.fillPath()
}) })

View File

@ -252,7 +252,6 @@ swift_library(
"//submodules/QrCodeUI:QrCodeUI", "//submodules/QrCodeUI:QrCodeUI",
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent", "//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent", "//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/Translate:Translate",
"//submodules/TabBarUI:TabBarUI", "//submodules/TabBarUI:TabBarUI",
"//submodules/SoftwareVideo:SoftwareVideo", "//submodules/SoftwareVideo:SoftwareVideo",
"//submodules/ManagedFile:ManagedFile", "//submodules/ManagedFile:ManagedFile",
@ -266,6 +265,7 @@ swift_library(
"//submodules/MediaPickerUI:MediaPickerUI", "//submodules/MediaPickerUI:MediaPickerUI",
"//submodules/ChatMessageBackground:ChatMessageBackground", "//submodules/ChatMessageBackground:ChatMessageBackground",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen", "//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
"//submodules/TranslateUI:TranslateUI",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -60,7 +60,7 @@ import InviteLinksUI
import Markdown import Markdown
import TelegramPermissionsUI import TelegramPermissionsUI
import Speak import Speak
import Translate import TranslateUI
import UniversalMediaPlayer import UniversalMediaPlayer
import WallpaperBackgroundNode import WallpaperBackgroundNode
import ChatListUI import ChatListUI
@ -2915,7 +2915,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
window.rootViewController?.present(controller, animated: true) window.rootViewController?.present(controller, animated: true)
} }
case .speak: case .speak:
speakText(text.string) let _ = speakText(text.string)
case .translate: case .translate:
let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|> take(1) |> 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) 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 }, displayImportedMessageTooltip: { [weak self] _ in
@ -10630,7 +10638,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if inputIsActive { if inputIsActive {
Queue.mainQueue().after(0.1, { Queue.mainQueue().after(0.15, {
present() present()
}) })
} else { } else {

View File

@ -25,7 +25,7 @@ import AdUI
import TelegramNotices import TelegramNotices
import ReactionListContextMenuContent import ReactionListContextMenuContent
import TelegramUIPreferences import TelegramUIPreferences
import Translate import TranslateUI
import DebugSettingsUI import DebugSettingsUI
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import Pasteboard import Pasteboard

View File

@ -2316,7 +2316,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string text = current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)).string
return (current, inputMode) return (current, inputMode)
} }
speakText(text) let _ = speakText(text)
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
UIMenuController.shared.hideMenu() UIMenuController.shared.hideMenu()

View File

@ -492,7 +492,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} }
} }
let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController) let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController)
present(controller, nil) Queue.mainQueue().after(0.3) {
present(controller, nil)
}
} }
} }
} }

View File

@ -63,7 +63,7 @@ import PasswordSetupUI
import CalendarMessageScreen import CalendarMessageScreen
import TooltipUI import TooltipUI
import QrCodeUI import QrCodeUI
import Translate import TranslateUI
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import CreateExternalMediaStreamScreen 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) let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
if canTranslate { if canTranslate {
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
translateText(context: context, text: text, fromLang: language)
let controller = TranslateScreen(context: context, text: text, fromLanguage: language)
self?.controller?.present(controller, in: .window(.root))
})) }))
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -10,14 +10,21 @@ import ActivityIndicator
import ChatListSearchItemNode import ChatListSearchItemNode
import ShimmerEffect import ShimmerEffect
struct LocalizationListItemEditing: Equatable { public struct LocalizationListItemEditing: Equatable {
let editable: Bool let editable: Bool
let editing: Bool let editing: Bool
let revealed: Bool let revealed: Bool
let reorderable: 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 presentationData: ItemListPresentationData
let id: String let id: String
let title: String let title: String
@ -26,13 +33,14 @@ class LocalizationListItem: ListViewItem, ItemListItem {
let activity: Bool let activity: Bool
let loading: Bool let loading: Bool
let editing: LocalizationListItemEditing let editing: LocalizationListItemEditing
let sectionId: ItemListSectionId let enabled: Bool
public let sectionId: ItemListSectionId
let alwaysPlain: Bool let alwaysPlain: Bool
let action: () -> Void let action: () -> Void
let setItemWithRevealedOptions: (String?, String?) -> Void let setItemWithRevealedOptions: (String?, String?) -> Void
let removeItem: (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.presentationData = presentationData
self.id = id self.id = id
self.title = title self.title = title
@ -41,6 +49,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
self.activity = activity self.activity = activity
self.loading = loading self.loading = loading
self.editing = editing self.editing = editing
self.enabled = enabled
self.sectionId = sectionId self.sectionId = sectionId
self.alwaysPlain = alwaysPlain self.alwaysPlain = alwaysPlain
self.action = action self.action = action
@ -48,7 +57,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
self.removeItem = removeItem self.removeItem = removeItem
} }
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async { async {
let node = LocalizationListItemNode() let node = LocalizationListItemNode()
var neighbors = itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem) 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 { Queue.mainQueue().async {
if let nodeValue = node() as? LocalizationListItemNode { if let nodeValue = node() as? LocalizationListItemNode {
let makeLayout = nodeValue.asyncLayout() 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) listView.clearHighlightAnimated(true)
self.action() self.action()
} }

View File

@ -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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -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<TextTranslationResult, TextTranslationError> {
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()
}
}
}

View File

@ -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<Empty>
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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"app": "8.6", "app": "8.6.1",
"bazel": "5.0.0", "bazel": "5.0.0",
"xcode": "13.2.1" "xcode": "13.2.1"
} }