mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-08 22:27:56 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
913 lines
42 KiB
Swift
913 lines
42 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import LocalizedPeerData
|
|
import TelegramStringFormatting
|
|
import TextFormat
|
|
import Markdown
|
|
import ChatPresentationInterfaceState
|
|
import AccountContext
|
|
import MoreButtonNode
|
|
import ContextUI
|
|
import TranslateUI
|
|
import TelegramUIPreferences
|
|
import TelegramNotices
|
|
import PremiumUI
|
|
|
|
final class ChatTranslationPanelNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
|
|
private let separatorNode: ASDisplayNode
|
|
|
|
private let button: HighlightableButtonNode
|
|
private let buttonIconNode: ASImageNode
|
|
private let buttonTextNode: ImmediateTextNode
|
|
private let moreButton: MoreButtonNode
|
|
private let closeButton: HighlightableButtonNode
|
|
|
|
private var theme: PresentationTheme?
|
|
|
|
private var chatInterfaceState: ChatPresentationInterfaceState?
|
|
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
|
|
|
init(context: AccountContext) {
|
|
self.context = context
|
|
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.isLayerBacked = true
|
|
|
|
self.button = HighlightableButtonNode()
|
|
self.buttonIconNode = ASImageNode()
|
|
self.buttonIconNode.displaysAsynchronously = false
|
|
|
|
self.buttonTextNode = ImmediateTextNode()
|
|
self.buttonTextNode.displaysAsynchronously = false
|
|
|
|
self.moreButton = MoreButtonNode(theme: context.sharedContext.currentPresentationData.with { $0 }.theme)
|
|
self.moreButton.iconNode.enqueueState(.more, animated: false)
|
|
self.moreButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
|
|
|
self.closeButton = HighlightableButtonNode()
|
|
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
|
self.closeButton.displaysAsynchronously = false
|
|
|
|
super.init()
|
|
|
|
self.clipsToBounds = true
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
self.addSubnode(self.button)
|
|
self.addSubnode(self.moreButton)
|
|
|
|
self.button.addSubnode(self.buttonIconNode)
|
|
self.button.addSubnode(self.buttonTextNode)
|
|
|
|
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside])
|
|
self.moreButton.action = { [weak self] _, gesture in
|
|
if let strongSelf = self {
|
|
strongSelf.morePressed(node: strongSelf.moreButton.contextSourceNode, gesture: gesture)
|
|
}
|
|
}
|
|
|
|
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
|
|
self.addSubnode(self.closeButton)
|
|
}
|
|
|
|
func animateOut() {
|
|
self.layer.animateBounds(from: self.bounds, to: self.bounds.offsetBy(dx: 0.0, dy: self.bounds.size.height), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
}
|
|
|
|
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat {
|
|
let previousIsEnabled = self.chatInterfaceState?.translationState?.isEnabled
|
|
self.chatInterfaceState = interfaceState
|
|
|
|
var themeUpdated = false
|
|
if interfaceState.theme !== self.theme {
|
|
themeUpdated = true
|
|
self.theme = interfaceState.theme
|
|
}
|
|
|
|
var isEnabledUpdated = false
|
|
if previousIsEnabled != interfaceState.translationState?.isEnabled {
|
|
isEnabledUpdated = true
|
|
}
|
|
|
|
if themeUpdated {
|
|
self.buttonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor)
|
|
self.moreButton.theme = interfaceState.theme
|
|
self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor
|
|
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: [])
|
|
}
|
|
|
|
if themeUpdated || isEnabledUpdated {
|
|
if previousIsEnabled != nil && isEnabledUpdated {
|
|
var offset: CGFloat = 30.0
|
|
if interfaceState.translationState?.isEnabled == false {
|
|
offset *= -1
|
|
}
|
|
if let snapshotView = self.button.view.snapshotContentTree() {
|
|
snapshotView.frame = self.button.frame
|
|
self.button.supernode?.view.addSubview(snapshotView)
|
|
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true)
|
|
self.button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.button.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true)
|
|
}
|
|
}
|
|
|
|
var languageCode = interfaceState.strings.baseLanguageCode
|
|
let rawSuffix = "-raw"
|
|
if languageCode.hasSuffix(rawSuffix) {
|
|
languageCode = String(languageCode.dropLast(rawSuffix.count))
|
|
}
|
|
|
|
let toLang = interfaceState.translationState?.toLang ?? languageCode
|
|
let key = "Translation.Language.\(toLang)"
|
|
let translateTitle: String
|
|
if let string = interfaceState.strings.primaryComponent.dict[key] {
|
|
translateTitle = interfaceState.strings.Conversation_Translation_TranslateTo(string).string
|
|
} else {
|
|
let languageLocale = Locale(identifier: languageCode)
|
|
let toLanguage = languageLocale.localizedString(forLanguageCode: toLang) ?? ""
|
|
translateTitle = interfaceState.strings.Conversation_Translation_TranslateToOther(toLanguage).string
|
|
}
|
|
|
|
let buttonText = interfaceState.translationState?.isEnabled == true ? interfaceState.strings.Conversation_Translation_ShowOriginal : translateTitle
|
|
self.buttonTextNode.attributedText = NSAttributedString(string: buttonText, font: Font.regular(17.0), textColor: interfaceState.theme.rootController.navigationBar.accentTextColor)
|
|
}
|
|
|
|
let panelHeight: CGFloat = 40.0
|
|
|
|
let contentRightInset: CGFloat = 14.0 + rightInset
|
|
|
|
let moreButtonSize = self.moreButton.measure(CGSize(width: 100.0, height: panelHeight))
|
|
self.moreButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - moreButtonSize.width, y: floorToScreenPixels((panelHeight - moreButtonSize.height) / 2.0)), size: moreButtonSize)
|
|
|
|
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
|
self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)
|
|
|
|
// MARK: Swiftgram
|
|
// if interfaceState.isPremium {
|
|
self.moreButton.isHidden = false
|
|
self.closeButton.isHidden = true
|
|
/* } else {
|
|
self.moreButton.isHidden = true
|
|
self.closeButton.isHidden = false
|
|
}*/
|
|
|
|
let buttonPadding: CGFloat = 10.0
|
|
let buttonSpacing: CGFloat = 10.0
|
|
let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: width - contentRightInset - moreButtonSize.width, height: panelHeight))
|
|
if let icon = self.buttonIconNode.image {
|
|
let buttonSize = CGSize(width: buttonTextSize.width + icon.size.width + buttonSpacing + buttonPadding * 2.0, height: panelHeight)
|
|
self.button.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - buttonSize.width) / 2.0), y: 0.0), size: buttonSize)
|
|
self.buttonIconNode.frame = CGRect(origin: CGPoint(x: buttonPadding, y: floorToScreenPixels((buttonSize.height - icon.size.height) / 2.0)), size: icon.size)
|
|
self.buttonTextNode.frame = CGRect(origin: CGPoint(x: buttonPadding + icon.size.width + buttonSpacing, y: floorToScreenPixels((buttonSize.height - buttonTextSize.height) / 2.0)), size: buttonTextSize)
|
|
}
|
|
|
|
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
|
|
|
|
return panelHeight
|
|
}
|
|
|
|
@objc private func closePressed() {
|
|
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: self.context.sharedContext.accountManager, count: -100, timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 7).startStandalone()
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
guard let translationState = self.chatInterfaceState?.translationState else {
|
|
return
|
|
}
|
|
// MARK: Swiftgram
|
|
let isPremium = self.chatInterfaceState?.isPremium ?? false
|
|
if isPremium || true {
|
|
self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated)
|
|
} else if !translationState.isEnabled {
|
|
let context = self.context
|
|
var replaceImpl: ((ViewController) -> Void)?
|
|
let controller = PremiumDemoScreen(context: context, subject: .translation, action: {
|
|
let controller = PremiumIntroScreen(context: context, source: .translation)
|
|
replaceImpl?(controller)
|
|
})
|
|
replaceImpl = { [weak controller] c in
|
|
controller?.replace(with: c)
|
|
}
|
|
self.interfaceInteraction?.chatController()?.push(controller)
|
|
}
|
|
}
|
|
|
|
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
|
|
guard let translationState = self.chatInterfaceState?.translationState else {
|
|
return
|
|
}
|
|
|
|
let context = self.context
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
var languageCode = presentationData.strings.baseLanguageCode
|
|
let rawSuffix = "-raw"
|
|
if languageCode.hasSuffix(rawSuffix) {
|
|
languageCode = String(languageCode.dropLast(rawSuffix.count))
|
|
}
|
|
|
|
let doNotTranslateTitle: String
|
|
let fromLang = translationState.fromLang
|
|
let key = "Translation.Language.\(fromLang)"
|
|
if let string = presentationData.strings.primaryComponent.dict[key] {
|
|
doNotTranslateTitle = presentationData.strings.Conversation_Translation_DoNotTranslate(string).string
|
|
} else {
|
|
let languageLocale = Locale(identifier: languageCode)
|
|
let fromLanguage = languageLocale.localizedString(forLanguageCode: fromLang) ?? ""
|
|
doNotTranslateTitle = presentationData.strings.Conversation_Translation_DoNotTranslateOther(fromLanguage).string
|
|
}
|
|
|
|
let items: Signal<ContextController.Items, NoError> = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
|
|> take(1)
|
|
|> map { sharedData -> ContextController.Items in
|
|
let settings: TranslationSettings
|
|
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
|
settings = current
|
|
} else {
|
|
settings = TranslationSettings.defaultSettings
|
|
}
|
|
|
|
var items: [ContextMenuItem] = []
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_Translation_ChooseLanguage, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor)
|
|
}, action: { c, _ in
|
|
var addedLanguages = Set<String>()
|
|
|
|
var topLanguages: [String] = []
|
|
let langCode = normalizeTranslationLanguage(languageCode)
|
|
|
|
var selectedLanguages: Set<String>
|
|
if let ignoredLanguages = settings.ignoredLanguages {
|
|
selectedLanguages = Set(ignoredLanguages)
|
|
} else {
|
|
selectedLanguages = Set([langCode])
|
|
for language in systemLanguageCodes() {
|
|
selectedLanguages.insert(language)
|
|
}
|
|
}
|
|
for code in supportedTranslationLanguages {
|
|
if selectedLanguages.contains(code) {
|
|
topLanguages.append(code)
|
|
}
|
|
}
|
|
|
|
topLanguages.append("")
|
|
|
|
var languages: [(String, String)] = []
|
|
let languageLocale = Locale(identifier: langCode)
|
|
|
|
for code in topLanguages {
|
|
if !addedLanguages.contains(code) {
|
|
let displayTitle = languageLocale.localizedString(forLanguageCode: code) ?? ""
|
|
let value = (code, displayTitle)
|
|
if code == languageCode {
|
|
languages.insert(value, at: 0)
|
|
} else {
|
|
languages.append(value)
|
|
}
|
|
addedLanguages.insert(code)
|
|
}
|
|
}
|
|
|
|
for code in supportedTranslationLanguages {
|
|
if !addedLanguages.contains(code) {
|
|
let displayTitle = languageLocale.localizedString(forLanguageCode: code) ?? ""
|
|
let value = (code, displayTitle)
|
|
if code == languageCode {
|
|
languages.insert(value, at: 0)
|
|
} else {
|
|
languages.append(value)
|
|
}
|
|
addedLanguages.insert(code)
|
|
}
|
|
}
|
|
|
|
c?.pushItems(items: .single(ContextController.Items(
|
|
content: .custom(
|
|
TranslationLanguagesContextMenuContent(
|
|
context: self.context,
|
|
languages: languages, back: { [weak c] in
|
|
c?.popItems()
|
|
}, selectLanguage: { [weak self, weak c] language in
|
|
c?.dismiss(completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.interfaceInteraction?.changeTranslationLanguage(language)
|
|
})
|
|
}
|
|
)
|
|
)
|
|
)))
|
|
})))
|
|
|
|
items.append(.separator)
|
|
|
|
items.append(.action(ContextMenuActionItem(text: doNotTranslateTitle, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] c, _ in
|
|
c?.dismiss(completion: nil)
|
|
|
|
self?.interfaceInteraction?.addDoNotTranslateLanguage(translationState.fromLang)
|
|
})))
|
|
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_Translation_Hide, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] c, _ in
|
|
c?.dismiss(completion: nil)
|
|
|
|
self?.interfaceInteraction?.hideTranslationPanel()
|
|
})))
|
|
|
|
return ContextController.Items(content: .list(items))
|
|
}
|
|
|
|
if let controller = self.interfaceInteraction?.chatController() {
|
|
let contextController = ContextController(presentationData: presentationData, source: .reference(TranslationContextReferenceContentSource(controller: controller, sourceNode: node)), items: items, gesture: gesture)
|
|
self.interfaceInteraction?.presentGlobalOverlayController(contextController, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class TranslationContextReferenceContentSource: ContextReferenceContentSource {
|
|
private let controller: ViewController
|
|
private let sourceNode: ContextReferenceContentNode
|
|
|
|
var keepInPlace: Bool {
|
|
return true
|
|
}
|
|
|
|
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
|
|
self.controller = controller
|
|
self.sourceNode = sourceNode
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
|
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|
|
|
|
private let separatorHeight: CGFloat = 7.0
|
|
|
|
private final class TranslationLanguagesContextMenuContent: ContextControllerItemsContent {
|
|
private final class BackButtonNode: HighlightTrackingButtonNode {
|
|
let highlightBackgroundNode: ASDisplayNode
|
|
let titleLabelNode: ImmediateTextNode
|
|
let separatorNode: ASDisplayNode
|
|
let iconNode: ASImageNode
|
|
|
|
var action: (() -> Void)?
|
|
|
|
private var theme: PresentationTheme?
|
|
|
|
init() {
|
|
self.highlightBackgroundNode = ASDisplayNode()
|
|
self.highlightBackgroundNode.isAccessibilityElement = false
|
|
self.highlightBackgroundNode.alpha = 0.0
|
|
|
|
self.titleLabelNode = ImmediateTextNode()
|
|
self.titleLabelNode.isAccessibilityElement = false
|
|
self.titleLabelNode.maximumNumberOfLines = 1
|
|
self.titleLabelNode.isUserInteractionEnabled = false
|
|
|
|
self.iconNode = ASImageNode()
|
|
self.iconNode.isAccessibilityElement = false
|
|
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.isAccessibilityElement = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
self.addSubnode(self.highlightBackgroundNode)
|
|
self.addSubnode(self.titleLabelNode)
|
|
self.addSubnode(self.iconNode)
|
|
|
|
self.isAccessibilityElement = true
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.highlightBackgroundNode.alpha = 1.0
|
|
} else {
|
|
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
|
|
strongSelf.highlightBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
self.action?()
|
|
}
|
|
|
|
func update(size: CGSize, presentationData: PresentationData, isLast: Bool) {
|
|
let standardIconWidth: CGFloat = 32.0
|
|
let sideInset: CGFloat = 16.0
|
|
let iconSideInset: CGFloat = 12.0
|
|
|
|
if self.theme !== presentationData.theme {
|
|
self.theme = presentationData.theme
|
|
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: presentationData.theme.contextMenu.primaryColor)
|
|
|
|
self.accessibilityLabel = presentationData.strings.Common_Back
|
|
}
|
|
|
|
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
|
|
|
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
self.titleLabelNode.attributedText = NSAttributedString(string: presentationData.strings.Common_Back, font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
|
|
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: size.width - sideInset - standardIconWidth, height: 100.0))
|
|
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + 36.0, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
|
|
|
if let iconImage = self.iconNode.image {
|
|
let iconFrame = CGRect(origin: CGPoint(x: iconSideInset, y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size)
|
|
self.iconNode.frame = iconFrame
|
|
}
|
|
|
|
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
|
|
self.separatorNode.isHidden = isLast
|
|
}
|
|
}
|
|
|
|
private final class LanguagesListNode: ASDisplayNode, ASScrollViewDelegate {
|
|
private final class ItemNode: HighlightTrackingButtonNode {
|
|
let context: AccountContext
|
|
let highlightBackgroundNode: ASDisplayNode
|
|
let titleLabelNode: ImmediateTextNode
|
|
let separatorNode: ASDisplayNode
|
|
|
|
let action: () -> Void
|
|
|
|
private var language: String?
|
|
|
|
init(context: AccountContext, action: @escaping () -> Void) {
|
|
self.action = action
|
|
self.context = context
|
|
|
|
self.highlightBackgroundNode = ASDisplayNode()
|
|
self.highlightBackgroundNode.isAccessibilityElement = false
|
|
self.highlightBackgroundNode.alpha = 0.0
|
|
|
|
self.titleLabelNode = ImmediateTextNode()
|
|
self.titleLabelNode.isAccessibilityElement = false
|
|
self.titleLabelNode.maximumNumberOfLines = 1
|
|
self.titleLabelNode.isUserInteractionEnabled = false
|
|
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.isAccessibilityElement = false
|
|
|
|
super.init()
|
|
|
|
self.isAccessibilityElement = true
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
self.addSubnode(self.highlightBackgroundNode)
|
|
self.addSubnode(self.titleLabelNode)
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self, let language = strongSelf.language, !language.isEmpty else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.highlightBackgroundNode.alpha = 1.0
|
|
} else {
|
|
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
|
|
strongSelf.highlightBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
guard let language = self.language, !language.isEmpty else {
|
|
return
|
|
}
|
|
self.action()
|
|
}
|
|
|
|
private var displayTitle: String?
|
|
func update(size: CGSize, presentationData: PresentationData, language: String, displayTitle: String, isLast: Bool, syncronousLoad: Bool) {
|
|
let sideInset: CGFloat = 16.0
|
|
|
|
if self.language != language {
|
|
self.language = language
|
|
self.displayTitle = displayTitle
|
|
|
|
self.accessibilityLabel = "\(displayTitle)"
|
|
}
|
|
|
|
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
|
|
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
self.titleLabelNode.attributedText = NSAttributedString(string: self.displayTitle ?? "", font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
|
|
let maxTextWidth: CGFloat = size.width - sideInset
|
|
|
|
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0))
|
|
let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
|
self.titleLabelNode.frame = titleFrame
|
|
|
|
if language == "" {
|
|
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
|
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: separatorHeight))
|
|
self.separatorNode.isHidden = false
|
|
} else {
|
|
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
|
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))
|
|
self.separatorNode.isHidden = isLast
|
|
}
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let languages: [(String, String)]
|
|
private let requestUpdate: (LanguagesListNode, ContainedViewLayoutTransition) -> Void
|
|
private let requestUpdateApparentHeight: (LanguagesListNode, ContainedViewLayoutTransition) -> Void
|
|
private let selectLanguage: (String) -> Void
|
|
|
|
private let scrollNode: ASScrollNode
|
|
private var ignoreScrolling: Bool = false
|
|
private var animateIn: Bool = false
|
|
private var bottomScrollInset: CGFloat = 0.0
|
|
|
|
private var presentationData: PresentationData?
|
|
private var currentSize: CGSize?
|
|
private var apparentHeight: CGFloat = 0.0
|
|
|
|
private var itemNodes: [Int: ItemNode] = [:]
|
|
|
|
init(
|
|
context: AccountContext,
|
|
languages: [(String, String)],
|
|
requestUpdate: @escaping (LanguagesListNode, ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (LanguagesListNode, ContainedViewLayoutTransition) -> Void,
|
|
selectLanguage: @escaping (String) -> Void
|
|
) {
|
|
self.context = context
|
|
self.languages = languages
|
|
self.requestUpdate = requestUpdate
|
|
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
|
self.selectLanguage = selectLanguage
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
self.scrollNode.canCancelAllTouchesInViews = true
|
|
self.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
if #available(iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
self.scrollNode.clipsToBounds = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.scrollNode)
|
|
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
|
|
|
self.clipsToBounds = true
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if self.ignoreScrolling {
|
|
return
|
|
}
|
|
self.updateVisibleItems(animated: false, syncronousLoad: false)
|
|
|
|
if let size = self.currentSize {
|
|
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
|
|
apparentHeight = max(apparentHeight, 44.0)
|
|
apparentHeight = min(apparentHeight, size.height)
|
|
if self.apparentHeight != apparentHeight {
|
|
self.apparentHeight = apparentHeight
|
|
|
|
self.requestUpdateApparentHeight(self, .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) {
|
|
guard let size = self.currentSize else {
|
|
return
|
|
}
|
|
guard let presentationData = self.presentationData else {
|
|
return
|
|
}
|
|
let itemHeight: CGFloat = 44.0
|
|
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0)
|
|
|
|
var validIds = Set<Int>()
|
|
|
|
let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight)))
|
|
let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight))
|
|
|
|
var separatorIndex = 0
|
|
for i in 0 ..< self.languages.count {
|
|
if self.languages[i].0.isEmpty {
|
|
separatorIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if minVisibleIndex <= maxVisibleIndex {
|
|
for index in minVisibleIndex ... maxVisibleIndex {
|
|
if index < self.languages.count {
|
|
let height = self.languages[index].0.isEmpty ? separatorHeight : itemHeight
|
|
var itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: height))
|
|
if index > separatorIndex {
|
|
itemFrame.origin.y += separatorHeight - itemHeight
|
|
}
|
|
|
|
let (languageCode, displayTitle) = self.languages[index]
|
|
validIds.insert(index)
|
|
|
|
let itemNode: ItemNode
|
|
if let current = self.itemNodes[index] {
|
|
itemNode = current
|
|
} else {
|
|
let selectLanguage = self.selectLanguage
|
|
itemNode = ItemNode(context: self.context, action: {
|
|
selectLanguage(languageCode)
|
|
})
|
|
self.itemNodes[index] = itemNode
|
|
self.scrollNode.addSubnode(itemNode)
|
|
}
|
|
|
|
itemNode.update(size: itemFrame.size, presentationData: presentationData, language: languageCode, displayTitle: displayTitle, isLast: index == self.languages.count - 1 || index == separatorIndex - 1, syncronousLoad: syncronousLoad)
|
|
itemNode.frame = itemFrame
|
|
}
|
|
}
|
|
}
|
|
|
|
var removeIds: [Int] = []
|
|
for (id, itemNode) in self.itemNodes {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
itemNode.removeFromSupernode()
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.itemNodes.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
var extendedScrollNodeFrame = self.scrollNode.frame
|
|
extendedScrollNodeFrame.size.height += self.bottomScrollInset
|
|
|
|
if extendedScrollNodeFrame.contains(point) {
|
|
return self.scrollNode.view.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event)
|
|
}
|
|
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
func update(presentationData: PresentationData, constrainedSize: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) {
|
|
let itemHeight: CGFloat = 44.0
|
|
|
|
self.presentationData = presentationData
|
|
|
|
var separatorIndex = 0
|
|
for i in 0 ..< self.languages.count {
|
|
if self.languages[i].0.isEmpty {
|
|
separatorIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
var contentHeight: CGFloat
|
|
if separatorIndex != 0 {
|
|
contentHeight = CGFloat(self.languages.count - 1) * itemHeight + separatorHeight
|
|
} else {
|
|
contentHeight = CGFloat(self.languages.count) * itemHeight
|
|
}
|
|
let size = CGSize(width: constrainedSize.width, height: contentHeight)
|
|
|
|
let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height))
|
|
self.currentSize = containerSize
|
|
|
|
self.ignoreScrolling = true
|
|
|
|
if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) {
|
|
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
|
}
|
|
if self.scrollNode.view.contentInset.bottom != bottomInset {
|
|
self.scrollNode.view.contentInset.bottom = bottomInset
|
|
}
|
|
self.bottomScrollInset = bottomInset
|
|
let scrollContentSize = CGSize(width: size.width, height: size.height)
|
|
if self.scrollNode.view.contentSize != scrollContentSize {
|
|
self.scrollNode.view.contentSize = scrollContentSize
|
|
}
|
|
self.ignoreScrolling = false
|
|
|
|
self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated)
|
|
|
|
self.animateIn = false
|
|
|
|
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
|
|
apparentHeight = max(apparentHeight, 44.0)
|
|
apparentHeight = min(apparentHeight, containerSize.height)
|
|
self.apparentHeight = apparentHeight
|
|
|
|
return (containerSize.height, apparentHeight)
|
|
}
|
|
}
|
|
|
|
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode {
|
|
private let context: AccountContext
|
|
private let languages: [(String, String)]
|
|
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
|
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
|
|
|
|
private var presentationData: PresentationData
|
|
|
|
private var backButtonNode: BackButtonNode?
|
|
private var separatorNode: ASDisplayNode?
|
|
|
|
private let currentTabIndex: Int = 0
|
|
private var visibleTabNodes: [Int: LanguagesListNode] = [:]
|
|
|
|
private let selectLanguage: (String) -> Void
|
|
|
|
private(set) var apparentHeight: CGFloat = 0.0
|
|
|
|
init(
|
|
context: AccountContext,
|
|
languages: [(String, String)],
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
back: (() -> Void)?,
|
|
selectLanguage: @escaping (String) -> Void
|
|
) {
|
|
self.context = context
|
|
self.languages = languages
|
|
self.selectLanguage = selectLanguage
|
|
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
|
|
|
self.requestUpdate = requestUpdate
|
|
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
|
|
|
if let back = back {
|
|
self.backButtonNode = BackButtonNode()
|
|
self.backButtonNode?.action = {
|
|
back()
|
|
}
|
|
}
|
|
|
|
super.init()
|
|
|
|
if self.backButtonNode != nil {
|
|
self.separatorNode = ASDisplayNode()
|
|
}
|
|
|
|
if let backButtonNode = self.backButtonNode {
|
|
self.addSubnode(backButtonNode)
|
|
}
|
|
if let separatorNode = self.separatorNode {
|
|
self.addSubnode(separatorNode)
|
|
}
|
|
}
|
|
|
|
func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) {
|
|
let constrainedSize = CGSize(width: min(220.0, constrainedWidth), height: min(604.0, maxHeight))
|
|
|
|
var topContentHeight: CGFloat = 0.0
|
|
if let backButtonNode = self.backButtonNode {
|
|
let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
|
|
backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: true)
|
|
transition.updateFrame(node: backButtonNode, frame: backButtonFrame)
|
|
topContentHeight += backButtonFrame.height
|
|
}
|
|
if let separatorNode = self.separatorNode {
|
|
let separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: separatorHeight))
|
|
separatorNode.backgroundColor = self.presentationData.theme.contextMenu.sectionSeparatorColor
|
|
transition.updateFrame(node: separatorNode, frame: separatorFrame)
|
|
topContentHeight += separatorFrame.height
|
|
}
|
|
|
|
var tabLayouts: [Int: (height: CGFloat, apparentHeight: CGFloat)] = [:]
|
|
|
|
var visibleIndices: [Int] = []
|
|
visibleIndices.append(self.currentTabIndex)
|
|
|
|
let previousVisibleTabFrames: [(Int, CGRect)] = self.visibleTabNodes.map { key, value -> (Int, CGRect) in
|
|
return (key, value.frame)
|
|
}
|
|
|
|
for index in visibleIndices {
|
|
var tabTransition = transition
|
|
let tabNode: LanguagesListNode
|
|
var initialReferenceFrame: CGRect?
|
|
if let current = self.visibleTabNodes[index] {
|
|
tabNode = current
|
|
} else {
|
|
for (previousIndex, previousFrame) in previousVisibleTabFrames {
|
|
if index > previousIndex {
|
|
initialReferenceFrame = previousFrame.offsetBy(dx: constrainedSize.width, dy: 0.0)
|
|
} else {
|
|
initialReferenceFrame = previousFrame.offsetBy(dx: -constrainedSize.width, dy: 0.0)
|
|
}
|
|
break
|
|
}
|
|
|
|
tabNode = LanguagesListNode(
|
|
context: self.context,
|
|
languages: self.languages,
|
|
requestUpdate: { [weak self] tab, transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
|
|
strongSelf.requestUpdate(transition)
|
|
}
|
|
},
|
|
requestUpdateApparentHeight: { [weak self] tab, transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
|
|
strongSelf.requestUpdateApparentHeight(transition)
|
|
}
|
|
},
|
|
selectLanguage: self.selectLanguage
|
|
)
|
|
self.addSubnode(tabNode)
|
|
self.visibleTabNodes[index] = tabNode
|
|
tabTransition = .immediate
|
|
}
|
|
|
|
let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), bottomInset: bottomInset, transition: tabTransition)
|
|
tabLayouts[index] = tabLayout
|
|
let currentFractionalTabIndex = CGFloat(self.currentTabIndex)
|
|
let xOffset: CGFloat = (CGFloat(index) - currentFractionalTabIndex) * constrainedSize.width
|
|
let tabFrame = CGRect(origin: CGPoint(x: xOffset, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: tabLayout.height))
|
|
tabTransition.updateFrame(node: tabNode, frame: tabFrame)
|
|
if let initialReferenceFrame = initialReferenceFrame {
|
|
transition.animatePositionAdditive(node: tabNode, offset: CGPoint(x: initialReferenceFrame.minX - tabFrame.minX, y: 0.0))
|
|
}
|
|
}
|
|
|
|
var contentSize = CGSize(width: constrainedSize.width, height: topContentHeight)
|
|
var apparentHeight = topContentHeight
|
|
|
|
if let tabLayout = tabLayouts[self.currentTabIndex] {
|
|
contentSize.height += tabLayout.height
|
|
apparentHeight += tabLayout.apparentHeight
|
|
}
|
|
|
|
return (contentSize, apparentHeight)
|
|
}
|
|
}
|
|
|
|
let context: AccountContext
|
|
let languages: [(String, String)]
|
|
let back: (() -> Void)?
|
|
let selectLanguage: (String) -> Void
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
languages: [(String, String)],
|
|
back: (() -> Void)?,
|
|
selectLanguage: @escaping (String) -> Void
|
|
) {
|
|
self.context = context
|
|
self.languages = languages
|
|
self.back = back
|
|
self.selectLanguage = selectLanguage
|
|
}
|
|
|
|
func node(
|
|
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
|
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
|
) -> ContextControllerItemsNode {
|
|
return ItemsNode(
|
|
context: self.context,
|
|
languages: self.languages,
|
|
requestUpdate: requestUpdate,
|
|
requestUpdateApparentHeight: requestUpdateApparentHeight,
|
|
back: self.back,
|
|
selectLanguage: self.selectLanguage
|
|
)
|
|
}
|
|
}
|