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.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.";

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
return (current, inputMode)
}
speakText(text)
let _ = speakText(text)
if #available(iOS 13.0, *) {
UIMenuController.shared.hideMenu()

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ public final class Button: Component {
public let content: AnyComponent<Empty>
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<Empty>,
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
}

View File

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

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")
}
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, 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<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,8 +21,6 @@ import SparseItemGrid
import UndoUI
import PresentationDataUtils
let overflowInset: CGFloat = 0.0
final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, 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<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) {
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ import AdUI
import TelegramNotices
import ReactionListContextMenuContent
import TelegramUIPreferences
import Translate
import TranslateUI
import DebugSettingsUI
import ChatPresentationInterfaceState
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
return (current, inputMode)
}
speakText(text)
let _ = speakText(text)
if #available(iOS 13.0, *) {
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)
present(controller, nil)
Queue.mainQueue().after(0.3) {
present(controller, nil)
}
}
}
}

View File

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

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

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",
"xcode": "13.2.1"
}