mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '974a2f6caa'
# Conflicts: # submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift # submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift # submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/ChatTranslationPanelNode.swift # submodules/TelegramUI/Sources/ChatControllerNode.swift
This commit is contained in:
commit
586f5039f1
211 changed files with 7852 additions and 11333 deletions
BIN
Telegram/Telegram-iOS/Resources/Cocoon.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Cocoon.tgs
Normal file
Binary file not shown.
|
|
@ -15622,6 +15622,28 @@ Error: %8$@";
|
|||
"Chat.GiftPurchaseOffer.AcceptConfirmation.BadValue" = "The value of this gift is **%@** higher than the offer.";
|
||||
"Chat.GiftPurchaseOffer.AcceptConfirmation.Confirm" = "Confirm Sale";
|
||||
|
||||
|
||||
"Notification.PremiumGift.DaysTitle_1" = "%@ Day Premium";
|
||||
"Notification.PremiumGift.DaysTitle_any" = "%@ Days Premium";
|
||||
|
||||
"MediaEditor.Audio.Title" = "Audio";
|
||||
"MediaEditor.Audio.SavedMusic" = "SAVED MUSIC";
|
||||
"MediaEditor.Audio.ShowMore" = "Show More";
|
||||
|
||||
"WebApp.AddToAttachmentTitle" = "Add to Attachment Menu";
|
||||
|
||||
"Gift.Upgrade.Wearable.Title" = "Wearable";
|
||||
"Gift.Upgrade.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses.";
|
||||
"Gift.Upgrade.ViewAllVariants" = "View all variants";
|
||||
|
||||
"CocoonInfo.Title" = "COCOON";
|
||||
"CocoonInfo.Description" = "**Cocoon** (**Co**nfidential **Co**mpute **O**pen **N**etwork) handles AI tasks **safely** and **efficiently**.";
|
||||
"CocoonInfo.Private.Title" = "Private";
|
||||
"CocoonInfo.Private.Text" = "No third party can access any data, such as translations, inside Cocoon.";
|
||||
"CocoonInfo.Efficient.Title" = "Efficient";
|
||||
"CocoonInfo.Efficient.Text" = "Cocoon has allowed Telegram to reduce translation costs by 6x.";
|
||||
"CocoonInfo.ForEveryone.Title" = "For Everyone";
|
||||
"CocoonInfo.ForEveryone.Text" = "Any developer can use Cocoon for AI features. Learn more at [@cocoon](telegram) or [cocoon.org](web).";
|
||||
"CocoonInfo.IntergrateInfo" = "Want to integrate Cocoon into your projects?\nReach out at [t.me/cocoon?direct]()";
|
||||
"CocoonInfo.Understood" = "Understood";
|
||||
|
||||
"Conversation.Translation.CocoonInfo" = "Translations are powered by\n**#Cocoon**. [How does it work?]()";
|
||||
|
|
|
|||
|
|
@ -1428,6 +1428,8 @@ public protocol SharedAccountContext: AnyObject {
|
|||
func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController
|
||||
func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController
|
||||
func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController
|
||||
func makeCocoonInfoScreen(context: AccountContext) -> ViewController
|
||||
func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController
|
||||
|
|
|
|||
|
|
@ -1144,7 +1144,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in
|
||||
let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in
|
||||
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
|
||||
if let link = link {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
|
||||
|
|
@ -1517,7 +1517,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
switch recognizer.state {
|
||||
case .began:
|
||||
if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] {
|
||||
let startX = itemView.frame.minX - 4.0
|
||||
let startX = itemView.frame.minX
|
||||
self.selectionGestureState = (startX, startX, itemId)
|
||||
|
||||
self.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
|
@ -2076,8 +2076,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
let panelSize = CGSize(width: isSelecting ? textPanelWidth : layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0, height: isSelecting ? textPanelHeight - 11.0 : glassPanelHeight)
|
||||
|
||||
let cornerRadius: CGFloat = isSelecting ? min(20.0, panelSize.height * 0.5) : panelSize.height * 0.5
|
||||
let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0)
|
||||
liquidLensView.update(size: panelSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 0.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition))
|
||||
liquidLensView.update(size: CGSize(width: panelSize.width, height: panelSize.height), cornerRadius: cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(panelSize.width - lensSelection.width, lensSelection.x)), y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height), inset: 3.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isSelecting, transition: ComponentTransition(transition))
|
||||
|
||||
transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5))
|
||||
transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: panelSize))
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Premium/PremiumCoinComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
"//submodules/PresentationDataUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import AsyncDisplayKit
|
|||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ProgressNavigationButtonNode
|
||||
import AccountContext
|
||||
|
||||
public final class AuthorizationSequenceCodeEntryController: ViewController {
|
||||
private var controllerNode: AuthorizationSequenceCodeEntryControllerNode {
|
||||
|
|
@ -14,6 +16,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController {
|
|||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let strings: PresentationStrings
|
||||
private let theme: PresentationTheme
|
||||
|
||||
|
|
@ -45,7 +48,8 @@ public final class AuthorizationSequenceCodeEntryController: ViewController {
|
|||
return self.data?.6 ?? false
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, back: @escaping () -> Void) {
|
||||
public init(sharedContext: SharedAccountContext, presentationData: PresentationData, back: @escaping () -> Void) {
|
||||
self.sharedContext = sharedContext
|
||||
self.strings = presentationData.strings
|
||||
self.theme = presentationData.theme
|
||||
|
||||
|
|
@ -61,11 +65,14 @@ public final class AuthorizationSequenceCodeEntryController: ViewController {
|
|||
return false
|
||||
}
|
||||
self.navigationBar?.backPressed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let text: String
|
||||
let proceed: String
|
||||
let stop: String
|
||||
|
||||
if let (_, _, type, _, _, _, _) = self?.data, case .email = type {
|
||||
if let (_, _, type, _, _, _, _) = self.data, case .email = type {
|
||||
text = presentationData.strings.Login_CancelEmailVerification
|
||||
proceed = presentationData.strings.Login_CancelEmailVerificationContinue
|
||||
stop = presentationData.strings.Login_CancelEmailVerificationStop
|
||||
|
|
@ -75,7 +82,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController {
|
|||
stop = presentationData.strings.Login_CancelPhoneVerificationStop
|
||||
}
|
||||
|
||||
self?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: proceed, action: {
|
||||
self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: proceed, action: {
|
||||
}), TextAlertAction(type: .defaultAction, title: stop, action: {
|
||||
back()
|
||||
})]), in: .window(.root))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import MtProtoKit
|
|||
import MessageUI
|
||||
import CoreTelephony
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
import CountrySelectionUI
|
||||
|
|
@ -247,7 +248,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
let carrier = CTCarrier()
|
||||
let mnc = carrier.mobileNetworkCode ?? "none"
|
||||
|
||||
AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData)
|
||||
AuthorizationSequenceController.presentEmailComposeController(sharedContext: strongSelf.sharedContext, address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_InvalidPhoneEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_InvalidPhoneEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData)
|
||||
}))
|
||||
case .phoneLimitExceeded:
|
||||
text = strongSelf.presentationData.strings.Login_PhoneFloodError
|
||||
|
|
@ -273,7 +274,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
let carrier = CTCarrier()
|
||||
let mnc = carrier.mobileNetworkCode ?? "none"
|
||||
|
||||
AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData)
|
||||
AuthorizationSequenceController.presentEmailComposeController(sharedContext: strongSelf.sharedContext, address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData)
|
||||
}))
|
||||
case let .generic(info):
|
||||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
|
|
@ -295,7 +296,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
errorString = "unknown"
|
||||
}
|
||||
|
||||
AuthorizationSequenceController.presentEmailComposeController(address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData)
|
||||
AuthorizationSequenceController.presentEmailComposeController(sharedContext: strongSelf.sharedContext, address: "recover@telegram.org", subject: strongSelf.presentationData.strings.Login_PhoneGenericEmailSubject(formattedNumber).string, body: strongSelf.presentationData.strings.Login_PhoneGenericEmailBody(formattedNumber, errorString, appVersion, systemVersion, locale, mnc).string, from: controller, presentationData: strongSelf.presentationData)
|
||||
}))
|
||||
case .timeout:
|
||||
text = strongSelf.presentationData.strings.Login_NetworkError
|
||||
|
|
@ -307,7 +308,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
controller.present(strongSelf.sharedContext.makeProxySettingsController(sharedContext: strongSelf.sharedContext, account: strongSelf.account), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}))
|
||||
}
|
||||
(controller.navigationController as? NavigationController)?.presentOverlay(controller: standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), inGlobal: true, blockInteraction: true)
|
||||
(controller.navigationController as? NavigationController)?.presentOverlay(controller: textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: actions), inGlobal: true, blockInteraction: true)
|
||||
|
||||
controller.dismissConfirmation()
|
||||
}
|
||||
|
|
@ -352,7 +353,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
@ -376,7 +377,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
if let currentController = currentController {
|
||||
controller = currentController
|
||||
} else {
|
||||
controller = AuthorizationSequenceCodeEntryController(presentationData: self.presentationData, back: { [weak self] in
|
||||
controller = AuthorizationSequenceCodeEntryController(sharedContext: self.sharedContext, presentationData: self.presentationData, back: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -451,7 +452,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone()
|
||||
}
|
||||
|
||||
controller.presentInGlobalOverlay(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]))
|
||||
controller.presentInGlobalOverlay(textAlertController(sharedContext: self.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]))
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -510,7 +511,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
controller.resetCode()
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -623,7 +624,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
controller.resetCode()
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -645,7 +646,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
let mnc = carrier.mobileNetworkCode ?? "none"
|
||||
let _ = strongSelf.engine.auth.reportMissingCode(phoneNumber: number, phoneCodeHash: phoneCodeHash, mnc: mnc).start()
|
||||
|
||||
AuthorizationSequenceController.presentDidNotGetCodeUI(controller: controller, presentationData: strongSelf.presentationData, phoneNumber: number, mnc: mnc)
|
||||
AuthorizationSequenceController.presentDidNotGetCodeUI(sharedContext: strongSelf.sharedContext, controller: controller, presentationData: strongSelf.presentationData, phoneNumber: number, mnc: mnc)
|
||||
}
|
||||
} else {
|
||||
controller?.inProgress = true
|
||||
|
|
@ -681,7 +682,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
text = strongSelf.presentationData.strings.Login_NetworkError
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: actions), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
@ -780,7 +781,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
text = strongSelf.presentationData.strings.Login_EmailNotAllowedError
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: {
|
||||
controller?.inProgress = false
|
||||
|
|
@ -827,7 +828,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
switch authorization.credential {
|
||||
case let appleIdCredential as ASAuthorizationAppleIDCredential:
|
||||
guard let tokenData = appleIdCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else {
|
||||
lastController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
lastController?.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -850,7 +851,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
case .emailNotAllowed:
|
||||
text = strongSelf.presentationData.strings.Login_EmailNotAllowedError
|
||||
}
|
||||
lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
lastController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
|
|
@ -891,7 +892,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
text = strongSelf.presentationData.strings.Login_InvalidEmailAddressError
|
||||
}
|
||||
|
||||
lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
lastController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -907,7 +908,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
guard let lastController = self.viewControllers.last as? ViewController else {
|
||||
return
|
||||
}
|
||||
lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: error.localizedDescription, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
lastController.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: error.localizedDescription, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
|
|
@ -927,7 +928,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
if let currentController = currentController {
|
||||
controller = currentController
|
||||
} else {
|
||||
controller = AuthorizationSequencePasswordEntryController(presentationData: self.presentationData, back: { [weak self] in
|
||||
controller = AuthorizationSequencePasswordEntryController(sharedContext: self.sharedContext, presentationData: self.presentationData, back: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -954,7 +955,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.passwordIsInvalid()
|
||||
}
|
||||
}
|
||||
|
|
@ -988,14 +989,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
|
||||
strongController.inProgress = false
|
||||
|
||||
strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongController.didForgotWithNoRecovery = true
|
||||
}))
|
||||
}
|
||||
}
|
||||
controller.reset = { [weak self, weak controller] in
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [
|
||||
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: {
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
|
|
@ -1015,7 +1016,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
case .limitExceeded:
|
||||
text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded
|
||||
}
|
||||
strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
@ -1082,7 +1083,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
})
|
||||
controller.reset = { [weak self, weak controller] in
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [
|
||||
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: {
|
||||
if let strongSelf = self, let strongController = controller {
|
||||
|
|
@ -1102,7 +1103,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
case .limitExceeded:
|
||||
text = strongSelf.presentationData.strings.Login_ResetAccountProtected_LimitExceeded
|
||||
}
|
||||
strongController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
@ -1132,7 +1133,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
if let currentController = currentController {
|
||||
controller = currentController
|
||||
} else {
|
||||
controller = AuthorizationSequenceSignUpController(presentationData: self.presentationData, back: { [weak self] in
|
||||
controller = AuthorizationSequenceSignUpController(sharedContext: self.sharedContext, presentationData: self.presentationData, back: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
@ -1231,7 +1232,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
@ -1379,7 +1380,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
}
|
||||
}
|
||||
|
||||
static func presentEmailComposeController(address: String, subject: String, body: String, from controller: ViewController, presentationData: PresentationData) {
|
||||
static func presentEmailComposeController(sharedContext: SharedAccountContext, address: String, subject: String, body: String, from controller: ViewController, presentationData: PresentationData) {
|
||||
if MFMailComposeViewController.canSendMail() {
|
||||
final class ComposeDelegate: NSObject, MFMailComposeViewControllerDelegate {
|
||||
@objc func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
|
|
@ -1398,7 +1399,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
|
||||
controller.view.window?.rootViewController?.present(composeController, animated: true, completion: nil)
|
||||
} else {
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(sharedContext: sharedContext, title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1451,6 +1452,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
}
|
||||
|
||||
public static func presentDidNotGetCodeUI(
|
||||
sharedContext: SharedAccountContext,
|
||||
controller: ViewController,
|
||||
presentationData: PresentationData,
|
||||
phoneNumber: String,
|
||||
|
|
@ -1470,6 +1472,6 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
|||
emailBody.append("Locale: \(locale)\n")
|
||||
emailBody.append("MNC: \(mnc)")
|
||||
|
||||
AuthorizationSequenceController.presentEmailComposeController(address: "sms@telegram.org", subject: presentationData.strings.Login_EmailCodeSubject(formattedNumber).string, body: emailBody, from: controller, presentationData: presentationData)
|
||||
AuthorizationSequenceController.presentEmailComposeController(sharedContext: sharedContext, address: "sms@telegram.org", subject: presentationData.strings.Login_EmailCodeSubject(formattedNumber).string, body: emailBody, from: controller, presentationData: presentationData)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import UIKit
|
|||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ProgressNavigationButtonNode
|
||||
import AccountContext
|
||||
|
||||
final class AuthorizationSequencePasswordEntryController: ViewController {
|
||||
private var controllerNode: AuthorizationSequencePasswordEntryControllerNode {
|
||||
|
|
@ -12,6 +14,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController {
|
|||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let presentationData: PresentationData
|
||||
|
||||
var loginWithPassword: ((String) -> Void)?
|
||||
|
|
@ -40,7 +43,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, back: @escaping () -> Void) {
|
||||
init(sharedContext: SharedAccountContext, presentationData: PresentationData, back: @escaping () -> Void) {
|
||||
self.sharedContext = sharedContext
|
||||
self.presentationData = presentationData
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
|
||||
|
|
@ -153,10 +157,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController {
|
|||
}
|
||||
|
||||
func forgotPressed() {
|
||||
/*if self.suggestReset {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryFailed, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
} else*/ if self.didForgotWithNoRecovery {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
if self.didForgotWithNoRecovery {
|
||||
self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
} else {
|
||||
self.forgot?()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||
).string
|
||||
|
||||
let presentationData = component.presentationData
|
||||
AuthorizationSequenceController.presentEmailComposeController(address: component.supportEmailAddress, subject: environment.strings.Login_PhonePaidEmailSubject, body: body, from: controller, presentationData: presentationData)
|
||||
AuthorizationSequenceController.presentEmailComposeController(sharedContext: component.sharedContext, address: component.supportEmailAddress, subject: environment.strings.Login_PhonePaidEmailSubject, body: body, from: controller, presentationData: presentationData)
|
||||
}
|
||||
|
||||
func update(component: AuthorizationSequencePaymentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import SwiftSignalKit
|
|||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ProgressNavigationButtonNode
|
||||
import AccountContext
|
||||
import CountrySelectionUI
|
||||
|
|
@ -404,7 +405,7 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
|
|||
}))
|
||||
}
|
||||
actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {}))
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root))
|
||||
self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root))
|
||||
} else {
|
||||
if let validLayout = self.validLayout, validLayout.size.width > 320.0 {
|
||||
let (code, formattedNumber) = self.controllerNode.formattedCodeAndNumber
|
||||
|
|
@ -425,7 +426,7 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
|
|||
strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts)
|
||||
}
|
||||
}))
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: logInNumber, text: self.presentationData.strings.Login_PhoneNumberConfirmation, actions: actions), in: .window(.root))
|
||||
self.present(textAlertController(sharedContext: self.sharedContext, title: logInNumber, text: self.presentationData.strings.Login_PhoneNumberConfirmation, actions: actions), in: .window(.root))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import AsyncDisplayKit
|
|||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import LegacyComponents
|
||||
import ProgressNavigationButtonNode
|
||||
import ImageCompression
|
||||
|
|
@ -13,6 +14,7 @@ import Postbox
|
|||
import TextFormat
|
||||
import MoreButtonNode
|
||||
import ContextUI
|
||||
import AccountContext
|
||||
|
||||
final class AuthorizationSequenceSignUpController: ViewController {
|
||||
private var controllerNode: AuthorizationSequenceSignUpControllerNode {
|
||||
|
|
@ -23,6 +25,7 @@ final class AuthorizationSequenceSignUpController: ViewController {
|
|||
|
||||
private let moreButtonNode: MoreButtonNode
|
||||
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let presentationData: PresentationData
|
||||
private let back: () -> Void
|
||||
|
||||
|
|
@ -46,7 +49,8 @@ final class AuthorizationSequenceSignUpController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, back: @escaping () -> Void, displayCancel: Bool) {
|
||||
init(sharedContext: SharedAccountContext, presentationData: PresentationData, back: @escaping () -> Void, displayCancel: Bool) {
|
||||
self.sharedContext = sharedContext
|
||||
self.presentationData = presentationData
|
||||
self.back = back
|
||||
|
||||
|
|
@ -68,7 +72,7 @@ final class AuthorizationSequenceSignUpController: ViewController {
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: {
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Login_CancelPhoneVerificationContinue, action: {
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_CancelPhoneVerificationStop, action: {
|
||||
back()
|
||||
})]), in: .window(.root))
|
||||
|
|
@ -92,7 +96,7 @@ final class AuthorizationSequenceSignUpController: ViewController {
|
|||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_CancelPhoneVerificationContinue, action: {
|
||||
self.present(textAlertController(sharedContext: self.sharedContext, title: nil, text: self.presentationData.strings.Login_CancelSignUpConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_CancelPhoneVerificationContinue, action: {
|
||||
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Login_CancelPhoneVerificationStop, action: { [weak self] in
|
||||
self?.back()
|
||||
})]), in: .window(.root))
|
||||
|
|
|
|||
|
|
@ -1320,24 +1320,28 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
|
||||
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
|
||||
var completed = false
|
||||
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: prompt, value: defaultText, apply: { value in
|
||||
if !completed {
|
||||
completed = true
|
||||
if let value = value {
|
||||
completionHandler(value)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
let promptController = promptController(
|
||||
context: self.context,
|
||||
updatedPresentationData: nil,
|
||||
text: prompt,
|
||||
value: defaultText,
|
||||
apply: { value in
|
||||
if !completed {
|
||||
completed = true
|
||||
if let value = value {
|
||||
completionHandler(value)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
promptController.dismissed = { byOutsideTap in
|
||||
if byOutsideTap {
|
||||
},
|
||||
dismissed: {
|
||||
if !completed {
|
||||
completed = true
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
self.present(promptController, nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2635,7 +2635,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
return
|
||||
}
|
||||
strongSelf.didSuggestAutoarchive = true
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Title, text: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Text, actions: [
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Title, text: strongSelf.presentationData.strings.ChatList_AutoarchiveSuggestion_Text, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -2687,7 +2687,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
return true
|
||||
}
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissTitle, text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissText(value), actions: [
|
||||
controller.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissTitle, text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissText(value), actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionCancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionOK, action: { [weak controller] in
|
||||
|
|
@ -3101,7 +3101,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
}
|
||||
|
||||
func displayContinueLiveStream() {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.ChatList_AlertResumeLiveStreamTitle, text: self.presentationData.strings.ChatList_AlertResumeLiveStreamText, actions: [
|
||||
self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_AlertResumeLiveStreamTitle, text: self.presentationData.strings.ChatList_AlertResumeLiveStreamText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatList_AlertResumeLiveStreamAction, action: { [weak self] in
|
||||
|
|
@ -4663,7 +4663,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
}
|
||||
|
||||
if hasLinks {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
|
||||
self.present(textAlertController(context: self.context, title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
confirmDeleteFolder()
|
||||
}),
|
||||
|
|
@ -5525,7 +5525,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteGroupConfirmation
|
||||
}
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
|
|
@ -5640,7 +5640,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
])
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
|
||||
|
|
@ -5708,7 +5708,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText
|
||||
}
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
|
|
@ -5916,7 +5916,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
completion(false)
|
||||
}),
|
||||
|
|
@ -5940,7 +5940,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
])
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
} else if peer.peerId == self.context.account.peerId {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||||
self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
|
||||
completion(false)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1752,14 +1752,14 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||
if initialPreset == nil {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = presentationData.strings.ChatListFilter_AlertCreateFolderBeforeSharingText
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
} else {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let state = stateValue.with({ $0 })
|
||||
if state.additionallyIncludePeers.isEmpty {
|
||||
let text = presentationData.strings.ChatListFilter_ErrorShareInvalidFolder
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -1784,7 +1784,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||
statusController?.dismiss()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -2359,7 +2359,7 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
|
|||
case .someUserTooManyChannels:
|
||||
text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit
|
||||
}
|
||||
presentController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
|
||||
presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import SwiftSignalKit
|
|||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
import ItemListPeerActionItem
|
||||
|
|
@ -516,13 +517,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||
}
|
||||
|
||||
if hasLinks {
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
|
||||
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
confirmDeleteFolder()
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
})
|
||||
]))
|
||||
], actionLayout: .vertical))
|
||||
} else {
|
||||
confirmDeleteFolder()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1352,7 +1352,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||
title = strongSelf.presentationData.strings.DownloadList_RemoveFileAlertTitle(Int32(messages.count))
|
||||
text = strongSelf.presentationData.strings.DownloadList_RemoveFileAlertText(Int32(messages.count))
|
||||
|
||||
strongSelf.present?(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [
|
||||
strongSelf.present?(textAlertController(context: strongSelf.context, title: title, text: text, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.DownloadList_RemoveFileAlertRemove, action: {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@ swift_library(
|
|||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/UrlEscaping:UrlEscaping",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/UrlEscaping",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -7,458 +7,90 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import UrlEscaping
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertMultilineInputFieldComponent
|
||||
|
||||
private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
fileprivate let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
public func chatTextLinkEditController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
text: String,
|
||||
link: String?,
|
||||
apply: @escaping (String?) -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
let inputState = AlertMultilineInputFieldComponent.ExternalState()
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
var textChanged: ((String) -> Void)?
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: link != nil ? strings.TextFormat_EditLinkTitle : strings.TextFormat_AddLinkTitle)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.TextFormat_AddLinkText(text).string))
|
||||
)
|
||||
))
|
||||
|
||||
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textInputNode.attributedText?.string ?? ""
|
||||
}
|
||||
set {
|
||||
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String = "" {
|
||||
didSet {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
|
||||
self.textInputNode = EditableTextNode()
|
||||
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.clipsToBounds = true
|
||||
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
|
||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.keyboardType = .URL
|
||||
self.textInputNode.autocapitalizationType = .none
|
||||
self.textInputNode.returnKeyType = .done
|
||||
self.textInputNode.autocorrectionType = .no
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.delegate = self
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.textInputNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
self.textInputNode.resignFirstResponder()
|
||||
}
|
||||
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(editableTextNode.textView.text)
|
||||
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
self.textInputNode.attributedText = nil
|
||||
self.deactivateInput()
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let text: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
let inputFieldNode: ChatTextLinkEditInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
private var isEditing = false
|
||||
private let allowEmpty: Bool
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, link: String?, allowEmpty: Bool) {
|
||||
self.strings = strings
|
||||
self.text = text
|
||||
self.isEditing = link != nil
|
||||
self.allowEmpty = allowEmpty
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
self.inputFieldNode = ChatTextLinkEditInputFieldNode(theme: ptheme, placeholder: strings.TextFormat_AddLinkPlaceholder)
|
||||
self.inputFieldNode.text = link ?? ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = !(link ?? "").isEmpty
|
||||
if allowEmpty {
|
||||
self.actionNodes.last?.actionEnabled = true
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.inputFieldNode.textChanged = { [weak self] text in
|
||||
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
|
||||
if strongSelf.allowEmpty {
|
||||
lastNode.actionEnabled = true
|
||||
} else {
|
||||
lastNode.actionEnabled = !text.isEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
if (link ?? "").isEmpty {
|
||||
Queue.mainQueue().after(0.1, {
|
||||
let pasteboard = UIPasteboard.general
|
||||
if pasteboard.hasURLs {
|
||||
if let url = pasteboard.url?.absoluteString, !url.isEmpty {
|
||||
self.inputFieldNode.text = url
|
||||
if let lastNode = self.actionNodes.last {
|
||||
lastNode.actionEnabled = true
|
||||
}
|
||||
self.inputFieldNode.textInputNode.textView.selectAll(nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var link: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.isEditing ? self.strings.TextFormat_EditLinkTitle : self.strings.TextFormat_AddLinkTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.strings.TextFormat_AddLinkText(self.text).string, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
public func chatTextLinkEditController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, account: Account, text: String, link: String?, allowEmpty: Bool = false, apply: @escaping (String?) -> Void) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertMultilineInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: link.flatMap { NSAttributedString(string: $0) },
|
||||
placeholder: strings.TextFormat_AddLinkPlaceholder,
|
||||
returnKeyType: .done,
|
||||
keyboardType: .URL,
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState,
|
||||
returnKeyAction: {
|
||||
applyImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = ChatTextLinkEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, link: link, allowEmpty: allowEmpty)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
let updatedLink = explicitUrl(contentNode.link)
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.Common_Done, type: .default, action: {
|
||||
applyImpl?()
|
||||
}, autoDismiss: false)
|
||||
],
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
applyImpl = {
|
||||
let updatedLink = explicitUrl(inputState.value.string)
|
||||
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) {
|
||||
dismissImpl?(true)
|
||||
dismissImpl?()
|
||||
apply(updatedLink)
|
||||
} else if allowEmpty && contentNode.link.isEmpty {
|
||||
dismissImpl?(true)
|
||||
} else if inputState.value.string.isEmpty {
|
||||
dismissImpl?()
|
||||
apply("")
|
||||
} else {
|
||||
contentNode.animateError()
|
||||
inputState.animateError()
|
||||
}
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
dismissImpl = { [weak alertController] in
|
||||
alertController?.dismiss(completion: nil)
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
contentNode.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
return alertController
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,20 @@ public final class Image: Component {
|
|||
public let tintColor: UIColor?
|
||||
public let size: CGSize?
|
||||
public let contentMode: UIImageView.ContentMode
|
||||
public let cornerRadius: CGFloat
|
||||
|
||||
public init(
|
||||
image: UIImage?,
|
||||
tintColor: UIColor? = nil,
|
||||
size: CGSize? = nil,
|
||||
contentMode: UIImageView.ContentMode = .scaleToFill
|
||||
contentMode: UIImageView.ContentMode = .scaleToFill,
|
||||
cornerRadius: CGFloat = 0.0
|
||||
) {
|
||||
self.image = image
|
||||
self.tintColor = tintColor
|
||||
self.size = size
|
||||
self.contentMode = contentMode
|
||||
self.cornerRadius = cornerRadius
|
||||
}
|
||||
|
||||
public static func ==(lhs: Image, rhs: Image) -> Bool {
|
||||
|
|
@ -32,6 +35,9 @@ public final class Image: Component {
|
|||
if lhs.contentMode != rhs.contentMode {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +53,9 @@ public final class Image: Component {
|
|||
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.image = component.image
|
||||
self.contentMode = component.contentMode
|
||||
|
||||
self.clipsToBounds = component.cornerRadius > 0.0
|
||||
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius)
|
||||
transition.setTintColor(view: self, color: component.tintColor ?? .white)
|
||||
|
||||
switch component.contentMode {
|
||||
|
|
|
|||
|
|
@ -346,6 +346,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
|||
}
|
||||
|
||||
let titleColor: UIColor
|
||||
let linkColor = presentationData.theme.list.itemAccentColor
|
||||
switch self.item.textColor {
|
||||
case .primary:
|
||||
titleColor = presentationData.theme.contextMenu.primaryColor
|
||||
|
|
@ -365,6 +366,8 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
|||
return ChatTextInputStateTextAttribute(type: .bold, range: entity.range)
|
||||
} else if case .Italic = entity.type {
|
||||
return ChatTextInputStateTextAttribute(type: .italic, range: entity.range)
|
||||
} else if case .Url = entity.type {
|
||||
return ChatTextInputStateTextAttribute(type: .textUrl(""), range: entity.range)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
@ -375,9 +378,12 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
|
|||
], range: NSRange(location: 0, length: result.length))
|
||||
for attribute in inputStateText.attributes {
|
||||
if case .bold = attribute.type {
|
||||
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(presentationData.listsFontSize.baseDisplaySize), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
result.addAttribute(NSAttributedString.Key.font, value: titleBoldFont, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
} else if case .italic = attribute.type {
|
||||
result.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
} else if case .textUrl = attribute.type {
|
||||
result.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
result.addAttribute(NSAttributedString.Key.font, value: titleBoldFont, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
}
|
||||
}
|
||||
attributedText = result
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||
|
||||
let state = stateValue.with({ $0 })
|
||||
|
||||
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: presentationData.strings.FolderLinkScreen_NameLink_Title, titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in
|
||||
let promptController = promptController(context: context, updatedPresentationData: updatedPresentationData, text: presentationData.strings.FolderLinkScreen_NameLink_Title, titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in
|
||||
if let value {
|
||||
updateState { state in
|
||||
var state = state
|
||||
|
|
@ -763,15 +763,21 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||
|
||||
if hasChanges {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.FolderLinkScreen_SaveAlertTitle, text: presentationData.strings.FolderLinkScreen_SaveAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.FolderLinkScreen_SaveAlertActionDiscard, action: {
|
||||
f()
|
||||
dismissImpl?()
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? presentationData.strings.FolderLinkScreen_SaveAlertActionApply : presentationData.strings.FolderLinkScreen_SaveAlertActionContinue, action: {
|
||||
applyChangesImpl?()
|
||||
})
|
||||
]), nil)
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
title: presentationData.strings.FolderLinkScreen_SaveAlertTitle,
|
||||
text: presentationData.strings.FolderLinkScreen_SaveAlertText,
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.FolderLinkScreen_SaveAlertActionDiscard, action: {
|
||||
f()
|
||||
dismissImpl?()
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? presentationData.strings.FolderLinkScreen_SaveAlertActionApply : presentationData.strings.FolderLinkScreen_SaveAlertActionContinue, action: {
|
||||
applyChangesImpl?()
|
||||
})
|
||||
]
|
||||
)
|
||||
presentControllerImpl?(alertController, nil)
|
||||
return false
|
||||
} else {
|
||||
f()
|
||||
|
|
|
|||
|
|
@ -18,9 +18,17 @@ import TelegramStringFormatting
|
|||
import WallpaperResources
|
||||
import UrlEscaping
|
||||
|
||||
private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold)
|
||||
private let iconFont = Font.with(size: 28.0, design: .round, weight: .bold)
|
||||
|
||||
private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500))
|
||||
private let iconTextGlassBackgroundImage = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor(rgb: 0xFF9500).cgColor)
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 12.0)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
})
|
||||
|
||||
public final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
|
|
@ -273,7 +281,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
|
|||
var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)?
|
||||
var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
|
||||
let applyIconTextBackgroundImage = iconTextBackgroundImage
|
||||
let applyIconTextBackgroundImage = item.systemStyle == .glass ? iconTextGlassBackgroundImage : iconTextBackgroundImage
|
||||
|
||||
var primaryUrl: String?
|
||||
|
||||
|
|
@ -637,7 +645,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
|
|||
var iconImageApply: (() -> Void)?
|
||||
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let imageCorners = ImageCorners(radius: 6.0)
|
||||
let imageCorners = ImageCorners(radius: item.systemStyle == .glass ? 12.0 : 6.0, curve: item.systemStyle == .glass ? .continuous : .circular)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
}
|
||||
|
|
@ -754,7 +762,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode {
|
|||
}
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 12.0), size: CGSize(width: 40.0, height: 40.0))
|
||||
transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size))
|
||||
transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0) + 1.0 - UIScreenPixel, y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size))
|
||||
|
||||
let _ = iconTextApply()
|
||||
|
||||
|
|
|
|||
|
|
@ -1437,7 +1437,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
}
|
||||
|
||||
if asFile && hasHeic {
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: {
|
||||
controller.present(textAlertController(context: controller.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: {
|
||||
proceed(false)
|
||||
}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.MediaPicker_ConvertToJpeg, action: {
|
||||
proceed(true)
|
||||
|
|
@ -2043,26 +2043,26 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
|
||||
if let _ = item as? TGMediaPickerGalleryPhotoItem {
|
||||
if self.bannedSendPhotos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
} else if let _ = item as? TGMediaPickerGalleryVideoItem {
|
||||
if self.bannedSendVideos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
} else if let asset = item as? TGMediaAsset {
|
||||
if asset.isVideo {
|
||||
if self.bannedSendVideos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if self.bannedSendPhotos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -2221,26 +2221,26 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
if let self = self, let selectionState = self.interaction?.selectionState {
|
||||
if let _ = item as? TGMediaPickerGalleryPhotoItem {
|
||||
if self.bannedSendPhotos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
} else if let _ = item as? TGMediaPickerGalleryVideoItem {
|
||||
if self.bannedSendVideos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
} else if let asset = item as? TGMediaAsset {
|
||||
if asset.isVideo {
|
||||
if self.bannedSendVideos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if self.bannedSendPhotos != nil {
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ final class MediaPickerTitleView: UIView {
|
|||
totalHeight += subtitleSize.height
|
||||
}
|
||||
|
||||
let verticalOffset: CGFloat = self.glass ? 3.0 : 0.0
|
||||
let verticalOffset: CGFloat = self.glass ? -1.0 : 0.0
|
||||
let arrowOffset: CGFloat = self.glass ? 1.0 : 5.0
|
||||
|
||||
var totalWidth = titleSize.width
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ public func notificationSoundSelectionController(context: AccountContext, update
|
|||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [
|
||||
controller.present(textAlertController(context: context, title: presentationData.strings.PeerInfo_DeleteToneTitle, text: presentationData.strings.PeerInfo_DeleteToneText(title).string, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
updateState { state in
|
||||
var state = state
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ public final class TwoFactorDataInputScreen: ViewController {
|
|||
return
|
||||
}
|
||||
if values[0] != values[1] {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
||||
]), in: .window(.root))
|
||||
return
|
||||
|
|
@ -167,7 +167,7 @@ public final class TwoFactorDataInputScreen: ViewController {
|
|||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||||
}
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak statusController] in
|
||||
statusController?.dismiss()
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ public final class TwoFactorDataInputScreen: ViewController {
|
|||
return
|
||||
}
|
||||
if values[0] != values[1] {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_SetupPasswordConfirmFailed, actions: [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
||||
]), in: .window(.root))
|
||||
return
|
||||
|
|
@ -481,7 +481,7 @@ public final class TwoFactorDataInputScreen: ViewController {
|
|||
}
|
||||
switch strongSelf.mode {
|
||||
case let .emailAddress(password, hint, doneText):
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationSkip, action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -543,7 +543,7 @@ public final class TwoFactorDataInputScreen: ViewController {
|
|||
strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: "", doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation))
|
||||
}
|
||||
case let .passwordRecovery(recovery, _):
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertAction, action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1091,7 +1091,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
|
|||
text = presentationData.strings.Channel_EditAdmin_CannotEdit
|
||||
}
|
||||
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
}, transferOwnership: {
|
||||
let _ = (context.engine.data.get(
|
||||
|
|
@ -1104,19 +1104,30 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
|
|||
}
|
||||
|
||||
transferOwnershipDisposable.set((context.engine.peers.checkOwnershipTranfserAvailability(memberId: adminId) |> deliverOnMainQueue).start(error: { error in
|
||||
let controller = channelOwnershipTransferController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, member: member, initialError: error, present: { c, a in
|
||||
presentControllerImpl?(c, a)
|
||||
}, completion: { upgradedPeerId in
|
||||
if let upgradedPeerId = upgradedPeerId {
|
||||
upgradedToSupergroupImpl(upgradedPeerId, {
|
||||
let controller = channelOwnershipTransferController(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
peer: peer,
|
||||
member: member,
|
||||
initialError: error,
|
||||
present: { c, a in
|
||||
presentControllerImpl?(c, a)
|
||||
},
|
||||
push: { c in
|
||||
pushControllerImpl?(c)
|
||||
},
|
||||
completion: { upgradedPeerId in
|
||||
if let upgradedPeerId = upgradedPeerId {
|
||||
upgradedToSupergroupImpl(upgradedPeerId, {
|
||||
dismissImpl?()
|
||||
transferedOwnership(member.id)
|
||||
})
|
||||
} else {
|
||||
dismissImpl?()
|
||||
transferedOwnership(member.id)
|
||||
})
|
||||
} else {
|
||||
dismissImpl?()
|
||||
transferedOwnership(member.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
presentControllerImpl?(controller, nil)
|
||||
}))
|
||||
})
|
||||
|
|
@ -1617,20 +1628,21 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
|
|||
rightNavigationButton = nil
|
||||
footerItem = ChannelAdminAddBotFooterItem(theme: presentationData.theme, title: state.adminRights ? presentationData.strings.Bot_AddToChat_Add_AddAsAdmin : presentationData.strings.Bot_AddToChat_Add_AddAsMember, action: {
|
||||
if state.adminRights {
|
||||
let theme = AlertControllerTheme(presentationData: presentationData)
|
||||
let attributedTitle = NSAttributedString(string: presentationData.strings.Bot_AddToChat_Add_AdminAlertTitle, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
let text = isGroup ? presentationData.strings.Bot_AddToChat_Add_AdminAlertTextGroup(peerTitle).string : presentationData.strings.Bot_AddToChat_Add_AdminAlertTextChannel(peerTitle).string
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: theme.primaryColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
|
||||
|
||||
let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_AdminAlertAdd, action: {
|
||||
rightButtonActionImpl()
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
})], actionLayout: .vertical)
|
||||
presentControllerImpl?(controller, nil)
|
||||
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
title: presentationData.strings.Bot_AddToChat_Add_AdminAlertTitle,
|
||||
text: text,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_AdminAlertAdd, action: { rightButtonActionImpl()
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
|
||||
],
|
||||
actionLayout: .vertical
|
||||
)
|
||||
presentControllerImpl?(alertController, nil)
|
||||
} else {
|
||||
rightButtonActionImpl()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1132,22 +1132,26 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
|
|||
controller.navigationPresentation = .modal
|
||||
controller.setState(.custom(icon: .animation("BroadcastGroup"), title: presentationData.strings.BroadcastGroups_IntroTitle, subtitle: nil, text: presentationData.strings.BroadcastGroups_IntroText, buttonTitle: presentationData.strings.BroadcastGroups_Convert, secondaryButtonTitle: presentationData.strings.BroadcastGroups_Cancel, footerText: nil), animated: false)
|
||||
controller.proceed = { [weak controller] result in
|
||||
let attributedTitle = NSAttributedString(string: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
|
||||
let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
|
||||
|
||||
let alertController = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in
|
||||
controller?.dismiss()
|
||||
|
||||
let _ = (convertGroupToGigagroup(account: context.account, peerId: originalPeerId)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: true, action: { _ in return false }), nil)
|
||||
|
||||
dismissToChatController?()
|
||||
})
|
||||
})])
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title,
|
||||
text: presentationData.strings.BroadcastGroups_ConfirmationAlert_Text,
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in
|
||||
controller?.dismiss()
|
||||
|
||||
let _ = (convertGroupToGigagroup(account: context.account, peerId: originalPeerId)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: true, action: { _ in return false }), nil)
|
||||
|
||||
dismissToChatController?()
|
||||
})
|
||||
})
|
||||
]
|
||||
)
|
||||
controller?.present(alertController, in: .window(.root))
|
||||
}
|
||||
pushControllerImpl?(controller)
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EdgeEffect",
|
||||
"//submodules/TelegramUI/Components/Gifts/TableComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/PeerTableCellComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import TelegramCore
|
|||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import TelegramPresentationData
|
||||
import Markdown
|
||||
import AlertUI
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
|
||||
public func presentGiveawayInfoController(
|
||||
context: AccountContext,
|
||||
|
|
@ -23,6 +23,8 @@ public func presentGiveawayInfoController(
|
|||
peerIds.append(adminId)
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Messages.Message(id: messageId),
|
||||
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
|
||||
|
|
@ -121,22 +123,16 @@ public func presentGiveawayInfoController(
|
|||
channelsCount = 1 + giveawayResults.additionalChannelsCount
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
|
||||
|
||||
let timeZone = TimeZone.current
|
||||
let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings)
|
||||
|
||||
let title: String
|
||||
let text: String
|
||||
var text: String
|
||||
var warning: String?
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||
dismissImpl?()
|
||||
})]
|
||||
|
||||
var actions: [AlertScreen.Action] = [
|
||||
.init(title: presentationData.strings.Common_OK, type: .default)
|
||||
]
|
||||
|
||||
var additionalPrizes = ""
|
||||
if let prizeDescription, !prizeDescription.isEmpty {
|
||||
|
|
@ -322,278 +318,65 @@ public func presentGiveawayInfoController(
|
|||
case .refunded:
|
||||
result = ""
|
||||
warning = presentationData.strings.Chat_Giveaway_Info_Refunded
|
||||
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: {
|
||||
dismissImpl?()
|
||||
})]
|
||||
case .notWon:
|
||||
result = "**\(presentationData.strings.Chat_Giveaway_Info_DidntWin)**\n\n"
|
||||
case let .wonPremium(slug):
|
||||
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
|
||||
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
|
||||
dismissImpl?()
|
||||
openLink(slug)
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
})]
|
||||
actions = [
|
||||
.init(title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, type: .default, action: {
|
||||
openLink(slug)
|
||||
}),
|
||||
.init(title: presentationData.strings.Common_Cancel)
|
||||
]
|
||||
case let .wonStars(stars):
|
||||
let _ = stars
|
||||
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
|
||||
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
|
||||
dismissImpl?()
|
||||
openLink("")
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
})]
|
||||
actions = [
|
||||
.init(title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, type: .default, action: {
|
||||
openLink("")
|
||||
}),
|
||||
.init(title: presentationData.strings.Common_Cancel)
|
||||
]
|
||||
}
|
||||
|
||||
text = "\(result)\(intro)\(additionalPrizes)\n\n\(ending)"
|
||||
}
|
||||
|
||||
let alertController = giveawayInfoAlertController(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
title: title,
|
||||
text: text,
|
||||
warning: warning,
|
||||
actions: actions
|
||||
)
|
||||
dismissImpl = { [weak alertController] in
|
||||
alertController?.dismissAnimated()
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
if let warning {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "warning",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(warning), color: .destructive, style: .background(.bold))
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
|
||||
content: content,
|
||||
actions: actions,
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
present(alertController)
|
||||
})
|
||||
}
|
||||
|
||||
private final class GiveawayInfoAlertContentNode: AlertContentNode {
|
||||
private let title: String
|
||||
private let text: String
|
||||
private let warning: String?
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
fileprivate let warningBackgroundNode: ASImageNode
|
||||
fileprivate let warningTextNode: ImmediateTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
public var theme: PresentationTheme
|
||||
|
||||
public override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) {
|
||||
self.theme = ptheme
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.warning = warning
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 0
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.warningBackgroundNode = ASImageNode()
|
||||
self.warningBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.warningTextNode = ImmediateTextNode()
|
||||
self.warningTextNode.maximumNumberOfLines = 0
|
||||
self.warningTextNode.lineSpacing = 0.1
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.warningBackgroundNode)
|
||||
self.addSubnode(self.warningTextNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
public override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
|
||||
|
||||
self.textNode.attributedText = attributedText
|
||||
|
||||
self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center)
|
||||
self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1))
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
if "".isEmpty {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
var warningHeight: CGFloat = 0.0
|
||||
if let _ = self.warning {
|
||||
let warningSize = self.warningTextNode.updateLayout(measureSize)
|
||||
let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize)
|
||||
transition.updateFrame(node: self.warningTextNode, frame: warningFrame)
|
||||
|
||||
transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0))
|
||||
|
||||
warningHeight += warningSize.height + 26.0
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions)
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1133,7 +1133,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||
|
||||
let closeButton = closeButton.update(
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 40.0, height: 40.0),
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: UIColor(rgb: 0x7f76f4),
|
||||
isDark: false,
|
||||
state: .tintedGlass,
|
||||
|
|
@ -1147,7 +1147,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
|
|
|
|||
|
|
@ -81,18 +81,6 @@ public func textAlertController(
|
|||
return controller
|
||||
}
|
||||
|
||||
public func textAlertController(
|
||||
sharedContext: SharedAccountContext,
|
||||
title: String?,
|
||||
text: String,
|
||||
actions: [TextAlertAction],
|
||||
actionLayout: TextAlertContentActionLayout = .horizontal,
|
||||
allowInputInset: Bool = true,
|
||||
dismissOnOutsideTap: Bool = true
|
||||
) -> AlertController {
|
||||
return textAlertController(alertContext: AlertControllerContext(theme: AlertControllerTheme(presentationData: sharedContext.currentPresentationData.with { $0 }), themeSignal: sharedContext.presentationData |> map { presentationData in AlertControllerTheme(presentationData: presentationData) }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, dismissOnOutsideTap: dismissOnOutsideTap)
|
||||
}
|
||||
|
||||
public func richTextAlertController(
|
||||
context: AccountContext,
|
||||
title: NSAttributedString?,
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@ swift_library(
|
|||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertMultilineInputFieldComponent
|
||||
|
||||
private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
|
|
@ -194,327 +197,95 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
|
|||
}
|
||||
}
|
||||
|
||||
private final class PromptAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let text: String
|
||||
private let titleFont: PromptControllerTitleFont
|
||||
private let subtitle: String?
|
||||
|
||||
private let textNode: ASTextNode
|
||||
private let subtitleNode: ASTextNode?
|
||||
let inputFieldNode: PromptInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, subtitle: String?, value: String?, placeholder: String?, characterLimit: Int, displayCharacterLimit: Bool) {
|
||||
self.strings = strings
|
||||
self.text = text
|
||||
self.titleFont = titleFont
|
||||
self.subtitle = subtitle
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
if subtitle != nil {
|
||||
let subtitleNode = ASTextNode()
|
||||
subtitleNode.maximumNumberOfLines = 0
|
||||
self.subtitleNode = subtitleNode
|
||||
} else {
|
||||
self.subtitleNode = nil
|
||||
}
|
||||
|
||||
self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: placeholder ?? "", characterLimit: characterLimit, displayCharacterLimit: displayCharacterLimit)
|
||||
self.inputFieldNode.text = value ?? ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
if let subtitleNode = self.subtitleNode {
|
||||
self.addSubnode(subtitleNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = true
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// self.inputFieldNode.textChanged = { [weak self] text in
|
||||
// if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
|
||||
// lastNode.actionEnabled = !text.isEmpty
|
||||
// }
|
||||
// }
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var value: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
let titleFontValue: UIFont
|
||||
switch self.titleFont {
|
||||
case .regular:
|
||||
titleFontValue = Font.regular(13.0)
|
||||
case .bold:
|
||||
titleFontValue = Font.semibold(17.0)
|
||||
}
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
if let subtitle = self.subtitle, let subtitleNode = self.subtitleNode {
|
||||
subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
}
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = CGSize()
|
||||
// transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
// origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
var subtitleSize: CGSize?
|
||||
if let subtitleNode {
|
||||
let subtitleSizeValue = subtitleNode.measure(measureSize)
|
||||
subtitleSize = subtitleSizeValue
|
||||
transition.updateFrame(node: subtitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSizeValue.width) / 2.0), y: origin.y), size: subtitleSizeValue))
|
||||
origin.y += subtitleSizeValue.height + 6.0 + spacing
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
if let subtitleSize {
|
||||
contentWidth = max(contentWidth, subtitleSize.width)
|
||||
}
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
var resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
if let subtitleSize {
|
||||
resultSize.height += subtitleSize.height + spacing
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
public enum PromptControllerTitleFont {
|
||||
case regular
|
||||
case bold
|
||||
}
|
||||
|
||||
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, subtitle: String? = nil, value: String?, placeholder: String? = nil, characterLimit: Int = 1000, displayCharacterLimit: Bool = false, apply: @escaping (String?) -> Void) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
|
||||
public func promptController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
text: String,
|
||||
titleFont: PromptControllerTitleFont = .regular,
|
||||
subtitle: String? = nil,
|
||||
value: String?,
|
||||
placeholder: String? = nil,
|
||||
characterLimit: Int = 1000,
|
||||
displayCharacterLimit: Bool = false,
|
||||
apply: @escaping (String?) -> Void,
|
||||
dismissed: @escaping () -> Void = {}
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||
dismissImpl?(true)
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, subtitle: subtitle, value: value, placeholder: placeholder, characterLimit: characterLimit, displayCharacterLimit: displayCharacterLimit)
|
||||
contentNode.complete = {
|
||||
dismissImpl?(true)
|
||||
applyImpl?()
|
||||
let inputState = AlertMultilineInputFieldComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
if subtitle == nil && titleFont == .regular {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
} else {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: text)
|
||||
)
|
||||
))
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
apply(contentNode.value)
|
||||
if let subtitle {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(subtitle))
|
||||
)
|
||||
))
|
||||
}
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertMultilineInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: value.flatMap { NSAttributedString(string: $0) },
|
||||
placeholder: placeholder ?? "",
|
||||
characterLimit: characterLimit,
|
||||
formatMenuAvailability: .none,
|
||||
emptyLineHandling: .notAllowed,
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
contentNode.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel, action: {
|
||||
apply(nil)
|
||||
}),
|
||||
.init(title: strings.Common_Done, type: .default, action: {
|
||||
apply(inputState.value.string)
|
||||
})
|
||||
],
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
alertController.dismissed = { byOutsideTap in
|
||||
if byOutsideTap {
|
||||
dismissed()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
return alertController
|
||||
}
|
||||
|
||||
private final class AuthAlertContentNode: AlertContentNode {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/SliderComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll
|
|||
controller?.inProgress = false
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, back: {
|
||||
let codeController = AuthorizationSequenceCodeEntryController(sharedContext: context.sharedContext, presentationData: presentationData, back: {
|
||||
dismissImpl?()
|
||||
})
|
||||
codeController.loginWithCode = { [weak codeController] code in
|
||||
|
|
@ -109,7 +109,7 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll
|
|||
let mnc = carrier.mobileNetworkCode ?? "none"
|
||||
let _ = context.engine.auth.reportMissingCode(phoneNumber: phoneNumber, phoneCodeHash: next.hash, mnc: mnc).start()
|
||||
|
||||
AuthorizationSequenceController.presentDidNotGetCodeUI(controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), phoneNumber: phoneNumber, mnc: mnc)
|
||||
AuthorizationSequenceController.presentDidNotGetCodeUI(sharedContext: context.sharedContext, controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), phoneNumber: phoneNumber, mnc: mnc)
|
||||
}
|
||||
codeController.openFragment = { url in
|
||||
context.sharedContext.applicationBindings.openUrl(url)
|
||||
|
|
@ -154,7 +154,15 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll
|
|||
|
||||
controller?.view.window?.rootViewController?.present(composeController, animated: true, completion: nil)
|
||||
} else {
|
||||
controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
title: nil,
|
||||
text: presentationData.strings.Login_EmailNotConfiguredError,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]
|
||||
)
|
||||
controller?.present(alertController, in: .window(.root))
|
||||
}
|
||||
}))
|
||||
case .generic:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import PresentationDataUtils
|
|||
import UrlEscaping
|
||||
|
||||
public final class ProxyServerActionSheetController: ActionSheetController {
|
||||
private let sharedContext: SharedAccountContext
|
||||
private var presentationDisposable: Disposable?
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
|
|
@ -25,10 +26,11 @@ public final class ProxyServerActionSheetController: ActionSheetController {
|
|||
|
||||
convenience public init(context: AccountContext, server: ProxyServerSettings) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.init(presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData)
|
||||
self.init(sharedContext: context.sharedContext, presentationData: presentationData, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, server: server, updatedPresentationData: context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal<PresentationData, NoError>?) {
|
||||
public init(sharedContext: SharedAccountContext, presentationData: PresentationData, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, server: ProxyServerSettings, updatedPresentationData: Signal<PresentationData, NoError>?) {
|
||||
self.sharedContext = sharedContext
|
||||
let sheetTheme = ActionSheetControllerTheme(presentationData: presentationData)
|
||||
super.init(theme: sheetTheme)
|
||||
|
||||
|
|
@ -39,7 +41,7 @@ public final class ProxyServerActionSheetController: ActionSheetController {
|
|||
items.append(ActionSheetTextItem(title: presentationData.strings.SocksProxySetup_AdNoticeHelp))
|
||||
}
|
||||
items.append(ProxyServerInfoItem(strings: presentationData.strings, network: network, server: server))
|
||||
items.append(ProxyServerActionItem(accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in
|
||||
items.append(ProxyServerActionItem(sharedContext: sharedContext, accountManager:accountManager, postbox: postbox, network: network, presentationData: presentationData, server: server, dismiss: { [weak self] success in
|
||||
guard let strongSelf = self, !strongSelf.isDismissed else {
|
||||
return
|
||||
}
|
||||
|
|
@ -262,6 +264,7 @@ private final class ProxyServerInfoItemNode: ActionSheetItemNode {
|
|||
}
|
||||
|
||||
private final class ProxyServerActionItem: ActionSheetItem {
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let accountManager: AccountManager<TelegramAccountManagerTypes>
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
|
|
@ -270,7 +273,8 @@ private final class ProxyServerActionItem: ActionSheetItem {
|
|||
private let dismiss: (Bool) -> Void
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
|
||||
init(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
init(sharedContext: SharedAccountContext, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, presentationData: PresentationData, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.sharedContext = sharedContext
|
||||
self.accountManager = accountManager
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
|
|
@ -281,7 +285,7 @@ private final class ProxyServerActionItem: ActionSheetItem {
|
|||
}
|
||||
|
||||
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
return ProxyServerActionItemNode(accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present)
|
||||
return ProxyServerActionItemNode(sharedContext: self.sharedContext, accountManager: self.accountManager, postbox: self.postbox, network: self.network, presentationData: self.presentationData, theme: theme, server: self.server, dismiss: self.dismiss, present: self.present)
|
||||
}
|
||||
|
||||
func updateNode(_ node: ActionSheetItemNode) {
|
||||
|
|
@ -289,6 +293,7 @@ private final class ProxyServerActionItem: ActionSheetItem {
|
|||
}
|
||||
|
||||
private final class ProxyServerActionItemNode: ActionSheetItemNode {
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let accountManager: AccountManager<TelegramAccountManagerTypes>
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
|
|
@ -305,7 +310,8 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode {
|
|||
private let disposable = MetaDisposable()
|
||||
private var revertSettings: ProxySettings?
|
||||
|
||||
init(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
init(sharedContext: SharedAccountContext, accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network, presentationData: PresentationData, theme: ActionSheetControllerTheme, server: ProxyServerSettings, dismiss: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.sharedContext = sharedContext
|
||||
self.accountManager = accountManager
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
|
|
@ -430,7 +436,7 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode {
|
|||
strongSelf.buttonNode.isUserInteractionEnabled = true
|
||||
strongSelf.requestLayoutUpdate()
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
||||
strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -7,465 +7,99 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import UrlEscaping
|
||||
import ActivityIndicator
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertInputFieldComponent
|
||||
|
||||
private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
fileprivate let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, apply: @escaping (String?) -> Void) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
var textChanged: ((String) -> Void)?
|
||||
let inputState = AlertInputFieldComponent.ExternalState()
|
||||
|
||||
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textInputNode.attributedText?.string ?? ""
|
||||
}
|
||||
set {
|
||||
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
}
|
||||
let doneIsEnabled: Signal<Bool, NoError> = inputState.valueSignal
|
||||
|> map { value in
|
||||
return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
var placeholder: String = "" {
|
||||
didSet {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String) {
|
||||
self.theme = theme
|
||||
let doneInProgressPromise = ValuePromise<Bool>(false)
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.WebBrowser_Exceptions_Create_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.WebBrowser_Exceptions_Create_Text))
|
||||
)
|
||||
))
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
|
||||
self.textInputNode = EditableTextNode()
|
||||
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.clipsToBounds = true
|
||||
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
|
||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.keyboardType = .URL
|
||||
self.textInputNode.autocapitalizationType = .none
|
||||
self.textInputNode.returnKeyType = .done
|
||||
self.textInputNode.autocorrectionType = .no
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.delegate = self
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.textInputNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
self.textInputNode.resignFirstResponder()
|
||||
}
|
||||
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(editableTextNode.textView.text)
|
||||
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||
}
|
||||
|
||||
private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: [])
|
||||
private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: [])
|
||||
|
||||
var inProgress = false
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if self.inProgress {
|
||||
return false
|
||||
}
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
}
|
||||
|
||||
if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex {
|
||||
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
|
||||
let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
|
||||
let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
|
||||
|
||||
if domainMatches.count > 0, pathMatches.count == 0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
self.textInputNode.attributedText = nil
|
||||
self.deactivateInput()
|
||||
}
|
||||
}
|
||||
|
||||
private final class WebBrowserDomainAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
let activityIndicator: ActivityIndicator
|
||||
let inputFieldNode: WebBrowserDomainInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow)
|
||||
self.activityIndicator.isHidden = true
|
||||
|
||||
self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder)
|
||||
self.inputFieldNode.text = ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
self.addSubnode(self.activityIndicator)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = false
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.inputFieldNode.textChanged = { [weak self] text in
|
||||
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
|
||||
lastNode.actionEnabled = !text.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var link: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: inputFrame)
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let activitySize = CGSize(width: 20.0, height: 20.0)
|
||||
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize))
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, apply: @escaping (String?) -> Void) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: [])
|
||||
let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: [])
|
||||
var applyImpl: (() -> Void)?
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: nil,
|
||||
placeholder: strings.WebBrowser_Exceptions_Create_Placeholder,
|
||||
characterLimit: nil,
|
||||
hasClearButton: true,
|
||||
keyboardType: .URL,
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState,
|
||||
shouldChangeText: { updatedText in
|
||||
guard let domainRegex, let pathRegex else {
|
||||
return true
|
||||
}
|
||||
let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
|
||||
let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
|
||||
if domainMatches.count > 0, pathMatches.count == 0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
returnKeyAction: {
|
||||
applyImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
var inProgress = false
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
if !inProgress {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
}
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||
if !inProgress {
|
||||
applyImpl?()
|
||||
}
|
||||
})]
|
||||
|
||||
let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
inProgress = true
|
||||
contentNode.inputFieldNode.inProgress = true
|
||||
contentNode.activityIndicator.isHidden = false
|
||||
|
||||
let updatedLink = explicitUrl(contentNode.link)
|
||||
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.Common_Done, type: .default, action: {
|
||||
applyImpl?()
|
||||
}, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get())
|
||||
],
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
applyImpl = {
|
||||
let updatedLink = explicitUrl(inputState.value)
|
||||
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) {
|
||||
doneInProgressPromise.set(true)
|
||||
apply(updatedLink)
|
||||
} else {
|
||||
contentNode.animateError()
|
||||
inputState.animateError()
|
||||
}
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
contentNode.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
return alertController
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl
|
|||
})
|
||||
dismissImpl = { [weak linkController] in
|
||||
linkController?.view.endEditing(true)
|
||||
linkController?.dismissAnimated()
|
||||
linkController?.dismiss(completion: nil)
|
||||
}
|
||||
controller?.present(linkController, in: .window(.root))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,8 +269,8 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
|
|||
} else {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime)
|
||||
presentControllerImpl?(standardTextAlertController(
|
||||
theme: AlertControllerTheme(presentationData: presentationData),
|
||||
presentControllerImpl?(textAlertController(
|
||||
context: context,
|
||||
title: presentationData.strings.GlobalAutodeleteSettings_SetConfirmTitle,
|
||||
text: presentationData.strings.GlobalAutodeleteSettings_SetConfirmText(valueText).string,
|
||||
actions: [
|
||||
|
|
@ -350,8 +350,8 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32,
|
|||
text = presentationData.strings.GlobalAutodeleteSettings_AttemptDisabledGenericSelection
|
||||
}
|
||||
|
||||
presentControllerImpl?(standardTextAlertController(
|
||||
theme: AlertControllerTheme(presentationData: presentationData),
|
||||
presentControllerImpl?(textAlertController(
|
||||
context: context,
|
||||
title: nil,
|
||||
text: text,
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import SwiftSignalKit
|
|||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import AuthorizationUI
|
||||
import AuthenticationServices
|
||||
import UndoUI
|
||||
|
|
@ -76,7 +77,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e
|
|||
var dismissCodeControllerImpl: (() -> Void)?
|
||||
var presentControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, back: {
|
||||
let codeController = AuthorizationSequenceCodeEntryController(sharedContext: context.sharedContext, presentationData: presentationData, back: {
|
||||
dismissCodeControllerImpl?()
|
||||
dismiss()
|
||||
})
|
||||
|
|
@ -119,7 +120,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e
|
|||
codeController?.resetCode()
|
||||
}
|
||||
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
|
||||
}
|
||||
}
|
||||
}, completed: { [weak codeController] in
|
||||
|
|
@ -148,7 +149,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e
|
|||
text = presentationData.strings.Login_EmailNotAllowedError
|
||||
}
|
||||
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
|
||||
presentControllerImpl?(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
|
||||
}, completed: { [weak emailController] in
|
||||
emailController?.inProgress = false
|
||||
})
|
||||
|
|
@ -173,7 +174,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e
|
|||
switch credential {
|
||||
case let appleIdCredential as ASAuthorizationAppleIDCredential:
|
||||
guard let tokenData = appleIdCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else {
|
||||
emailController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
emailController?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
return
|
||||
}
|
||||
let _ = (verifyLoginEmailChange(account: context.account, code: .appleToken(token))
|
||||
|
|
@ -193,7 +194,7 @@ public func loginEmailSetupController(context: AccountContext, blocking: Bool, e
|
|||
case .emailNotAllowed:
|
||||
text = presentationData.strings.Login_EmailNotAllowedError
|
||||
}
|
||||
emailController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
emailController?.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak emailController] in
|
||||
emailController?.authorization = nil
|
||||
emailController?.authorizationDelegate = nil
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import Display
|
|||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import ProgressNavigationButtonNode
|
||||
import AccountContext
|
||||
|
||||
public class TermsOfServiceControllerTheme {
|
||||
public let statusBarStyle: StatusBarStyle
|
||||
|
|
@ -43,6 +45,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont
|
|||
return self.displayNode as! TermsOfServiceControllerNode
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let presentationData: PresentationData
|
||||
private let text: String
|
||||
private let entities: [MessageTextEntity]
|
||||
|
|
@ -67,7 +70,8 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont
|
|||
}
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) {
|
||||
public init(context: AccountContext, presentationData: PresentationData, text: String, entities: [MessageTextEntity], ageConfirmation: Int32?, signingUp: Bool, accept: @escaping (String?) -> Void, decline: @escaping () -> Void, openUrl: @escaping (String) -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
|
|
@ -112,7 +116,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont
|
|||
text = strongSelf.presentationData.strings.PrivacyPolicy_DeclineMessage
|
||||
declineTitle = strongSelf.presentationData.strings.PrivacyPolicy_DeclineDeclineAndDelete
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: {
|
||||
self?.decline()
|
||||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
})], actionLayout: .vertical), in: .window(.root))
|
||||
|
|
@ -122,7 +126,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont
|
|||
}
|
||||
|
||||
if let ageConfirmation = strongSelf.ageConfirmation {
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationAgree, action: {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationTitle, text: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationMessage("\(ageConfirmation)").string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.PrivacyPolicy_AgeVerificationAgree, action: {
|
||||
self?.accept(self?.proccessBotNameAfterAccept)
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -49,13 +49,14 @@ swift_library(
|
|||
"//submodules/TelegramNotices",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
"//submodules/PasswordSetupUI",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
||||
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import SheetComponent
|
|||
import BundleIconComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import ButtonComponent
|
||||
import LottieComponent
|
||||
import AccountContext
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ private final class SheetContent: CombinedComponent {
|
|||
|
||||
let title = Child(BalancedTextComponent.self)
|
||||
let list = Child(List<Empty>.self)
|
||||
let actionButton = Child(SolidRoundedButtonComponent.self)
|
||||
let actionButton = Child(ButtonComponent.self)
|
||||
|
||||
let infoBackground = Child(RoundedRectangle.self)
|
||||
let infoTitle = Child(MultilineTextComponent.self)
|
||||
|
|
@ -284,7 +284,7 @@ private final class SheetContent: CombinedComponent {
|
|||
let infoBackground = infoBackground.update(
|
||||
component: RoundedRectangle(
|
||||
color: theme.list.blocksBackgroundColor,
|
||||
cornerRadius: 10.0
|
||||
cornerRadius: 26.0
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight),
|
||||
transition: .immediate
|
||||
|
|
@ -307,37 +307,48 @@ private final class SheetContent: CombinedComponent {
|
|||
contentSize.height += infoPadding
|
||||
contentSize.height += spacing
|
||||
|
||||
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
|
||||
buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "anim_ok"),
|
||||
color: theme.list.itemCheckColors.foregroundColor,
|
||||
startingPosition: .begin,
|
||||
size: CGSize(width: 28.0, height: 28.0),
|
||||
playOnce: state.playOnce
|
||||
))))
|
||||
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
|
||||
text: strings.Monetization_Intro_Understood,
|
||||
badge: 0,
|
||||
textColor: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: theme.list.itemCheckColors.fillColor
|
||||
))))
|
||||
|
||||
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
|
||||
let actionButton = actionButton.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: strings.Monetization_Intro_Understood,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: theme.list.itemCheckColors.fillColor,
|
||||
backgroundColors: [],
|
||||
foregroundColor: theme.list.itemCheckColors.foregroundColor
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
|
||||
),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
iconName: nil,
|
||||
animationName: nil,
|
||||
iconPosition: .left,
|
||||
action: {
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(actionButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += actionButton.size.height
|
||||
contentSize.height += 22.0
|
||||
|
||||
contentSize.height += environment.safeInsets.bottom
|
||||
|
||||
contentSize.height += buttonInsets.bottom
|
||||
|
||||
state.playAnimationIfNeeded()
|
||||
|
||||
return contentSize
|
||||
|
|
@ -397,6 +408,7 @@ private final class SheetContainerComponent: CombinedComponent {
|
|||
})
|
||||
}
|
||||
)),
|
||||
style: .glass,
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
externalState: sheetExternalState,
|
||||
|
|
|
|||
|
|
@ -6,107 +6,148 @@ import TelegramPresentationData
|
|||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import PasswordSetupUI
|
||||
import Markdown
|
||||
import OwnershipTransferController
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertInputFieldComponent
|
||||
|
||||
func confirmRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
func confirmRevenueWithdrawalController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
peerId: EnginePeer.Id,
|
||||
present: @escaping (ViewController, Any?) -> Void,
|
||||
completion: @escaping (String) -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
let inputState = AlertInputFieldComponent.ExternalState()
|
||||
|
||||
let doneIsEnabled: Signal<Bool, NoError> = inputState.valueSignal
|
||||
|> map { value in
|
||||
return !value.isEmpty
|
||||
}
|
||||
|
||||
let doneInProgressPromise = ValuePromise<Bool>(false)
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Monetization_Withdraw_EnterPassword_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.Monetization_Withdraw_EnterPassword_Text))
|
||||
)
|
||||
))
|
||||
|
||||
var applyImpl: (() -> Void)?
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertInputFieldComponent(
|
||||
context: context,
|
||||
placeholder: strings.Channel_OwnershipTransfer_PasswordPlaceholder,
|
||||
isSecureTextEntry: true,
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState,
|
||||
returnKeyAction: {
|
||||
applyImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
var proceedImpl: (() -> Void)?
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Title, text: presentationData.strings.Monetization_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Done, action: {
|
||||
proceedImpl?()
|
||||
})])
|
||||
|
||||
contentNode.complete = {
|
||||
proceedImpl?()
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.theme = presentationData.theme
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
disposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller, weak contentNode] in
|
||||
contentNode?.dismissInput()
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
proceedImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
contentNode.updateIsChecking(true)
|
||||
|
||||
let signal = context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: true, amount: nil, password: contentNode.password)
|
||||
disposable.set((signal |> deliverOnMainQueue).start(next: { url in
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.Monetization_Withdraw_EnterPassword_Done, type: .default, action: {
|
||||
applyImpl?()
|
||||
}, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get())
|
||||
],
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
applyImpl = {
|
||||
doneInProgressPromise.set(true)
|
||||
|
||||
let _ = (context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, ton: true, amount: nil, password: inputState.value)
|
||||
|> deliverOnMainQueue).start(next: { url in
|
||||
dismissImpl?()
|
||||
completion(url)
|
||||
}, error: { [weak contentNode] error in
|
||||
}, error: { error in
|
||||
var errorTextAndActions: (String, [TextAlertAction])?
|
||||
switch error {
|
||||
case .invalidPassword:
|
||||
contentNode?.animateError()
|
||||
case .limitExceeded:
|
||||
errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
default:
|
||||
errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
case .invalidPassword:
|
||||
inputState.animateError()
|
||||
case .limitExceeded:
|
||||
errorTextAndActions = (strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])
|
||||
default:
|
||||
errorTextAndActions = (strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])
|
||||
}
|
||||
contentNode?.updateIsChecking(false)
|
||||
|
||||
doneInProgressPromise.set(false)
|
||||
|
||||
if let (text, actions) = errorTextAndActions {
|
||||
dismissImpl?()
|
||||
present(textAlertController(context: context, title: nil, text: text, actions: actions), nil)
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
return controller
|
||||
dismissImpl = { [weak alertController] in
|
||||
alertController?.dismiss(completion: nil)
|
||||
}
|
||||
return alertController
|
||||
}
|
||||
|
||||
|
||||
public func revenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = AlertControllerTheme(presentationData: presentationData)
|
||||
let strings = presentationData.strings
|
||||
|
||||
var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
var title: String? = strings.OwnershipTransfer_SecurityCheck
|
||||
var text = strings.Monetization_Withdraw_SecurityRequirements
|
||||
|
||||
var text = presentationData.strings.Monetization_Withdraw_SecurityRequirements
|
||||
let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0
|
||||
|
||||
var actions: [TextAlertAction] = []
|
||||
var actions: [AlertScreen.Action] = [
|
||||
.init(title: strings.Common_OK, type: .default)
|
||||
]
|
||||
switch initialError {
|
||||
case .requestPassword:
|
||||
return confirmRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, present: present, completion: completion)
|
||||
case .twoStepAuthTooFresh, .authSessionTooFresh:
|
||||
text = text + presentationData.strings.Monetization_Withdraw_ComeBackLater
|
||||
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]
|
||||
case .twoStepAuthMissing:
|
||||
actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: {
|
||||
case .requestPassword:
|
||||
return confirmRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, present: present, completion: completion)
|
||||
case .twoStepAuthTooFresh, .authSessionTooFresh:
|
||||
text = text + strings.Monetization_Withdraw_ComeBackLater
|
||||
case .twoStepAuthMissing:
|
||||
actions = [
|
||||
.init(title: strings.OwnershipTransfer_SetupTwoStepAuth, type: .default, action: {
|
||||
let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in
|
||||
if shouldDismiss {
|
||||
controller.dismiss()
|
||||
}
|
||||
})
|
||||
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})]
|
||||
default:
|
||||
title = nil
|
||||
text = presentationData.strings.Login_UnknownError
|
||||
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]
|
||||
}),
|
||||
.init(title: strings.Common_Cancel)
|
||||
]
|
||||
default:
|
||||
title = nil
|
||||
text = strings.Login_UnknownError
|
||||
}
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor)
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
|
||||
|
||||
return richTextAlertController(context: context, title: title, text: attributedText, actions: actions)
|
||||
return AlertScreen(
|
||||
context: context,
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
|
||||
title: title,
|
||||
text: text,
|
||||
actions: actions
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import SheetComponent
|
|||
import BundleIconComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import LottieComponent
|
||||
import ButtonComponent
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import PremiumPeerShortcutComponent
|
||||
import GlassBarButtonComponent
|
||||
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
|
@ -55,18 +55,6 @@ private final class SheetContent: CombinedComponent {
|
|||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedCloseImage: (UIImage, PresentationTheme)?
|
||||
|
||||
let playOnce = ActionSlot<Void>()
|
||||
private var didPlayAnimation = false
|
||||
|
||||
func playAnimationIfNeeded() {
|
||||
guard !self.didPlayAnimation else {
|
||||
return
|
||||
}
|
||||
self.didPlayAnimation = true
|
||||
self.playOnce.invoke(Void())
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
|
|
@ -74,25 +62,23 @@ private final class SheetContent: CombinedComponent {
|
|||
}
|
||||
|
||||
static var body: Body {
|
||||
let closeButton = Child(Button.self)
|
||||
let closeButton = Child(GlassBarButtonComponent.self)
|
||||
|
||||
let amount = Child(MultilineTextComponent.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let date = Child(MultilineTextComponent.self)
|
||||
let peerShortcut = Child(PremiumPeerShortcutComponent.self)
|
||||
|
||||
let actionButton = Child(SolidRoundedButtonComponent.self)
|
||||
let actionButton = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let theme = environment.theme
|
||||
let strings = environment.strings
|
||||
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
|
||||
|
||||
let titleFont = Font.semibold(17.0)
|
||||
|
|
@ -103,26 +89,27 @@ private final class SheetContent: CombinedComponent {
|
|||
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 45.0)
|
||||
|
||||
let closeImage: UIImage
|
||||
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
|
||||
closeImage = image
|
||||
} else {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
|
||||
state.cachedCloseImage = (closeImage, theme)
|
||||
}
|
||||
|
||||
let closeButton = closeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(Image(image: closeImage)),
|
||||
action: { [weak component] in
|
||||
component?.dismiss()
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 40.0, height: 40.0),
|
||||
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
|
||||
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
let amountString: NSMutableAttributedString
|
||||
|
|
@ -255,27 +242,30 @@ private final class SheetContent: CombinedComponent {
|
|||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += peerShortcut.size.height
|
||||
contentSize.height += 50.0
|
||||
contentSize.height += 32.0
|
||||
} else {
|
||||
contentSize.height += 45.0
|
||||
contentSize.height += 27.0
|
||||
}
|
||||
|
||||
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
|
||||
let actionButton = actionButton.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: buttonTitle,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: theme.list.itemCheckColors.fillColor,
|
||||
backgroundColors: [],
|
||||
foregroundColor: theme.list.itemCheckColors.foregroundColor
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: buttonTitle,
|
||||
badge: 0,
|
||||
textColor: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: theme.list.itemCheckColors.fillColor
|
||||
))
|
||||
),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
iconName: nil,
|
||||
animationName: nil,
|
||||
iconPosition: .left,
|
||||
action: {
|
||||
component.dismiss()
|
||||
if let explorerUrl {
|
||||
|
|
@ -283,18 +273,14 @@ private final class SheetContent: CombinedComponent {
|
|||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(actionButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += actionButton.size.height
|
||||
contentSize.height += 22.0
|
||||
|
||||
contentSize.height += environment.safeInsets.bottom
|
||||
|
||||
state.playAnimationIfNeeded()
|
||||
contentSize.height += buttonInsets.bottom
|
||||
|
||||
return contentSize
|
||||
}
|
||||
|
|
@ -360,6 +346,7 @@ private final class SheetContainerComponent: CombinedComponent {
|
|||
})
|
||||
}
|
||||
)),
|
||||
style: .glass,
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
externalState: sheetExternalState,
|
||||
|
|
|
|||
|
|
@ -433,6 +433,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1548122514] = { return Api.InputNotifyPeer.parse_inputNotifyForumTopic($0) }
|
||||
dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) }
|
||||
dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) }
|
||||
dict[1528613672] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialFirebasePNV($0) }
|
||||
dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) }
|
||||
dict[-1021329078] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseLogin($0) }
|
||||
dict[1046713180] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseRegister($0) }
|
||||
|
|
@ -1421,7 +1422,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-1506535550] = { return Api.messages.DiscussionMessage.parse_discussionMessage($0) }
|
||||
dict[1155883043] = { return Api.messages.EmojiGameInfo.parse_emojiGameDiceInfo($0) }
|
||||
dict[1508266805] = { return Api.messages.EmojiGameInfo.parse_emojiGameUnavailable($0) }
|
||||
dict[-1585592191] = { return Api.messages.EmojiGameOutcome.parse_emojiGameOutcome($0) }
|
||||
dict[-634726841] = { return Api.messages.EmojiGameOutcome.parse_emojiGameOutcome($0) }
|
||||
dict[-2011186869] = { return Api.messages.EmojiGroups.parse_emojiGroups($0) }
|
||||
dict[1874111879] = { return Api.messages.EmojiGroups.parse_emojiGroupsNotModified($0) }
|
||||
dict[410107472] = { return Api.messages.ExportedChatInvite.parse_exportedChatInvite($0) }
|
||||
|
|
|
|||
|
|
@ -898,10 +898,17 @@ public extension Api {
|
|||
}
|
||||
public extension Api {
|
||||
enum InputPasskeyCredential: TypeConstructorDescription {
|
||||
case inputPasskeyCredentialFirebasePNV(pnvToken: String)
|
||||
case inputPasskeyCredentialPublicKey(id: String, rawId: String, response: Api.InputPasskeyResponse)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inputPasskeyCredentialFirebasePNV(let pnvToken):
|
||||
if boxed {
|
||||
buffer.appendInt32(1528613672)
|
||||
}
|
||||
serializeString(pnvToken, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputPasskeyCredentialPublicKey(let id, let rawId, let response):
|
||||
if boxed {
|
||||
buffer.appendInt32(1009235855)
|
||||
|
|
@ -915,11 +922,24 @@ public extension Api {
|
|||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .inputPasskeyCredentialFirebasePNV(let pnvToken):
|
||||
return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", pnvToken as Any)])
|
||||
case .inputPasskeyCredentialPublicKey(let id, let rawId, let response):
|
||||
return ("inputPasskeyCredentialPublicKey", [("id", id as Any), ("rawId", rawId as Any), ("response", response as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(pnvToken: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
|
|
|
|||
|
|
@ -1136,15 +1136,16 @@ public extension Api.messages {
|
|||
}
|
||||
public extension Api.messages {
|
||||
enum EmojiGameOutcome: TypeConstructorDescription {
|
||||
case emojiGameOutcome(seed: Buffer, tonAmount: Int64)
|
||||
case emojiGameOutcome(seed: Buffer, stakeTonAmount: Int64, tonAmount: Int64)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .emojiGameOutcome(let seed, let tonAmount):
|
||||
case .emojiGameOutcome(let seed, let stakeTonAmount, let tonAmount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1585592191)
|
||||
buffer.appendInt32(-634726841)
|
||||
}
|
||||
serializeBytes(seed, buffer: buffer, boxed: false)
|
||||
serializeInt64(stakeTonAmount, buffer: buffer, boxed: false)
|
||||
serializeInt64(tonAmount, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
|
|
@ -1152,8 +1153,8 @@ public extension Api.messages {
|
|||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .emojiGameOutcome(let seed, let tonAmount):
|
||||
return ("emojiGameOutcome", [("seed", seed as Any), ("tonAmount", tonAmount as Any)])
|
||||
case .emojiGameOutcome(let seed, let stakeTonAmount, let tonAmount):
|
||||
return ("emojiGameOutcome", [("seed", seed as Any), ("stakeTonAmount", stakeTonAmount as Any), ("tonAmount", tonAmount as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1162,10 +1163,13 @@ public extension Api.messages {
|
|||
_1 = parseBytes(reader)
|
||||
var _2: Int64?
|
||||
_2 = reader.readInt64()
|
||||
var _3: Int64?
|
||||
_3 = reader.readInt64()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.messages.EmojiGameOutcome.emojiGameOutcome(seed: _1!, tonAmount: _2!)
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.messages.EmojiGameOutcome.emojiGameOutcome(seed: _1!, stakeTonAmount: _2!, tonAmount: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||
"//submodules/TelegramUI/Components/BackButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/DirectMediaImageCache",
|
||||
"//submodules/FastBlur",
|
||||
|
|
@ -131,6 +130,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EdgeEffect",
|
||||
"//submodules/PremiumUI",
|
||||
"//submodules/TelegramUI/Components/Calls/VideoChatMicButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -188,11 +188,11 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry {
|
|||
case let .reasonsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .reason(_, reason, title, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in
|
||||
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleReason(reason, value)
|
||||
})
|
||||
case let .comment(_, text, placeholder):
|
||||
return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in
|
||||
return ItemListMultilineInputItem(presentationData: presentationData, systemStyle: .glass, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in
|
||||
arguments.updateComment(updatedText)
|
||||
}, updatedFocus: { focused in
|
||||
if focused {
|
||||
|
|
@ -200,7 +200,7 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry {
|
|||
}
|
||||
}, tag: CallFeedbackControllerEntryTag.comment)
|
||||
case let .includeLogs(_, title, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleIncludeLogs(value)
|
||||
})
|
||||
case let .includeLogsInfo(_, text):
|
||||
|
|
|
|||
|
|
@ -3,285 +3,13 @@ import UIKit
|
|||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramVoip
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
|
||||
private final class CallRatingAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let apply: (Int) -> Void
|
||||
|
||||
var rating: Int?
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private var starContainerNode: ASDisplayNode
|
||||
private let starNodes: [ASButtonNode]
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], dismiss: @escaping () -> Void, apply: @escaping (Int) -> Void) {
|
||||
self.strings = strings
|
||||
self.apply = apply
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 3
|
||||
|
||||
self.starContainerNode = ASDisplayNode()
|
||||
|
||||
var starNodes: [ASButtonNode] = []
|
||||
for _ in 0 ..< 5 {
|
||||
starNodes.append(ASButtonNode())
|
||||
}
|
||||
self.starNodes = starNodes
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
|
||||
self.addSubnode(self.starContainerNode)
|
||||
|
||||
for node in self.starNodes {
|
||||
node.addTarget(self, action: #selector(self.starPressed(_:)), forControlEvents: .touchDown)
|
||||
node.addTarget(self, action: #selector(self.starReleased(_:)), forControlEvents: .touchUpInside)
|
||||
self.starContainerNode.addSubnode(node)
|
||||
}
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.starContainerNode.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
@objc func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let location = gestureRecognizer.location(in: self.starContainerNode.view)
|
||||
var selectedNode: ASButtonNode?
|
||||
for node in self.starNodes {
|
||||
if node.frame.contains(location) {
|
||||
selectedNode = node
|
||||
break
|
||||
}
|
||||
}
|
||||
if let selectedNode = selectedNode {
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.starPressed(selectedNode)
|
||||
case .ended:
|
||||
self.starReleased(selectedNode)
|
||||
case .cancelled:
|
||||
self.resetStars()
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
self.resetStars()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetStars() {
|
||||
for i in 0 ..< self.starNodes.count {
|
||||
let node = self.starNodes[i]
|
||||
node.isSelected = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func starPressed(_ sender: ASButtonNode) {
|
||||
if let index = self.starNodes.firstIndex(of: sender) {
|
||||
self.rating = index + 1
|
||||
for i in 0 ..< self.starNodes.count {
|
||||
let node = self.starNodes[i]
|
||||
node.isSelected = i <= index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func starReleased(_ sender: ASButtonNode) {
|
||||
if let index = self.starNodes.firstIndex(of: sender) {
|
||||
self.rating = index + 1
|
||||
for i in 0 ..< self.starNodes.count {
|
||||
let node = self.starNodes[i]
|
||||
node.isSelected = i <= index
|
||||
}
|
||||
if let rating = self.rating {
|
||||
self.apply(rating)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.strings.Calls_RatingTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
for node in self.starNodes {
|
||||
node.setImage(generateTintedImage(image: UIImage(bundleImageName: "Call/Star"), color: theme.accentColor), for: [])
|
||||
let highlighted = generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: theme.accentColor)
|
||||
node.setImage(highlighted, for: [.selected])
|
||||
node.setImage(highlighted, for: [.selected, .highlighted])
|
||||
}
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width , 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((resultWidth - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 13.0
|
||||
|
||||
let starSize = CGSize(width: 42.0, height: 38.0)
|
||||
let starsOrigin = floorToScreenPixels((resultWidth - starSize.width * 5.0) / 2.0)
|
||||
self.starContainerNode.frame = CGRect(origin: CGPoint(x: starsOrigin, y: origin.y), size: CGSize(width: starSize.width * CGFloat(self.starNodes.count), height: starSize.height))
|
||||
for i in 0 ..< self.starNodes.count {
|
||||
let node = self.starNodes[i]
|
||||
transition.updateFrame(node: node, frame: CGRect(x: starSize.width * CGFloat(i), y: 0.0, width: starSize.width, height: starSize.height))
|
||||
}
|
||||
origin.y += titleSize.height
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + actionsHeight + 56.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
import AlertComponent
|
||||
|
||||
func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int, comment: String, userInitiated: Bool, includeLogs: Bool) -> Signal<Void, NoError> {
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4244000))
|
||||
|
|
@ -309,35 +37,199 @@ func rateCallAndSendLogs(engine: TelegramEngine, callId: CallId, starsCount: Int
|
|||
}
|
||||
}
|
||||
|
||||
public func callRatingController(sharedContext: SharedAccountContext, account: Account, callId: CallId, userInitiated: Bool, isVideo: Bool, present: @escaping (ViewController, Any) -> Void, push: @escaping (ViewController) -> Void) -> AlertController {
|
||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
let strings = presentationData.strings
|
||||
public func callRatingController(
|
||||
sharedContext: SharedAccountContext,
|
||||
account: Account,
|
||||
callId: CallId,
|
||||
userInitiated: Bool,
|
||||
isVideo: Bool,
|
||||
present: @escaping (ViewController, Any) -> Void,
|
||||
push: @escaping (ViewController) -> Void
|
||||
) -> ViewController {
|
||||
let strings = sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var contentNode: CallRatingAlertContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {
|
||||
dismissImpl?(true)
|
||||
})]
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
contentNode = CallRatingAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions, dismiss: {
|
||||
dismissImpl?(true)
|
||||
}, apply: { rating in
|
||||
dismissImpl?(true)
|
||||
if rating < 4 {
|
||||
push(callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated, isVideo: isVideo))
|
||||
} else {
|
||||
let _ = rateCallAndSendLogs(engine: TelegramEngine(account: account), callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start()
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(
|
||||
title: strings.Calls_RatingTitle,
|
||||
alignment: .center
|
||||
)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "stars",
|
||||
component: AnyComponent(
|
||||
AlertCallRatingComponent(completion: { rating in
|
||||
dismissImpl?()
|
||||
if rating < 4 {
|
||||
push(callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated, isVideo: isVideo))
|
||||
} else {
|
||||
let _ = rateCallAndSendLogs(engine: TelegramEngine(account: account), callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start()
|
||||
}
|
||||
})
|
||||
)
|
||||
))
|
||||
|
||||
let alertController = AlertScreen(
|
||||
sharedContext: sharedContext,
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_NotNow)
|
||||
]
|
||||
)
|
||||
|
||||
dismissImpl = { [weak alertController] in
|
||||
alertController?.dismiss(completion: nil)
|
||||
}
|
||||
|
||||
return alertController
|
||||
}
|
||||
|
||||
private final class AlertCallRatingComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
private let completion: (Int) -> Void
|
||||
|
||||
public init(completion: @escaping (Int) -> Void) {
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertCallRatingComponent, rhs: AlertCallRatingComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var containerView: UIView
|
||||
private let starButtons: [HighlightTrackingButton]
|
||||
|
||||
var rating: Int?
|
||||
|
||||
private var component: AlertCallRatingComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.containerView = UIView()
|
||||
|
||||
var starButtons: [HighlightTrackingButton] = []
|
||||
for _ in 0 ..< 5 {
|
||||
starButtons.append(HighlightTrackingButton())
|
||||
}
|
||||
self.starButtons = starButtons
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
for button in self.starButtons {
|
||||
button.addTarget(self, action: #selector(self.starPressed(_:)), for: .touchDown)
|
||||
button.addTarget(self, action: #selector(self.starReleased(_:)), for: .touchUpInside)
|
||||
self.containerView.addSubview(button)
|
||||
}
|
||||
|
||||
self.containerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
})
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let location = gestureRecognizer.location(in: self.containerView)
|
||||
var selectedButton: HighlightTrackingButton?
|
||||
for button in self.starButtons {
|
||||
if button.frame.contains(location) {
|
||||
selectedButton = button
|
||||
break
|
||||
}
|
||||
}
|
||||
if let selectedButton = selectedButton {
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.starPressed(selectedButton)
|
||||
case .ended:
|
||||
self.starReleased(selectedButton)
|
||||
case .cancelled:
|
||||
self.resetStars()
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
self.resetStars()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetStars() {
|
||||
for i in 0 ..< self.starButtons.count {
|
||||
let node = self.starButtons[i]
|
||||
node.isSelected = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func starPressed(_ sender: HighlightTrackingButton) {
|
||||
if let index = self.starButtons.firstIndex(of: sender) {
|
||||
self.rating = index + 1
|
||||
for i in 0 ..< self.starButtons.count {
|
||||
let node = self.starButtons[i]
|
||||
node.isSelected = i <= index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func starReleased(_ sender: HighlightTrackingButton) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let index = self.starButtons.firstIndex(of: sender) {
|
||||
self.rating = index + 1
|
||||
for i in 0 ..< self.starButtons.count {
|
||||
let node = self.starButtons[i]
|
||||
node.isSelected = i <= index
|
||||
}
|
||||
if let rating = self.rating {
|
||||
component.completion(rating)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AlertCallRatingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
if self.component == nil {
|
||||
for i in 0 ..< self.starButtons.count {
|
||||
let button = self.starButtons[i]
|
||||
button.setImage(UIImage(bundleImageName: "Call/Star")?.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
button.setImage(UIImage(bundleImageName: "Call/StarHighlighted")?.withRenderingMode(.alwaysTemplate), for: .selected)
|
||||
button.setImage(UIImage(bundleImageName: "Call/StarHighlighted")?.withRenderingMode(.alwaysTemplate), for: [.selected, .highlighted])
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let buttonCount = CGFloat(self.starButtons.count)
|
||||
let starSize = CGSize(width: 42.0, height: 38.0)
|
||||
let starsOrigin = floorToScreenPixels((availableSize.width - starSize.width * buttonCount) / 2.0)
|
||||
self.containerView.frame = CGRect(origin: CGPoint(x: starsOrigin, y: 0.0), size: CGSize(width: starSize.width * buttonCount, height: starSize.height))
|
||||
for i in 0 ..< self.starButtons.count {
|
||||
let button = self.starButtons[i]
|
||||
button.imageView?.tintColor = environment.theme.actionSheet.controlAccentColor
|
||||
|
||||
transition.setFrame(view: button, frame: CGRect(x: starSize.width * CGFloat(i), y: 0.0, width: starSize.width, height: starSize.height))
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: 38.0)
|
||||
}
|
||||
}
|
||||
return controller
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,237 +3,117 @@ import UIKit
|
|||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import BundleIconComponent
|
||||
|
||||
private final class CallSuggestTabAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
public func callSuggestTabController(sharedContext: SharedAccountContext) -> ViewController {
|
||||
let strings = sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "header",
|
||||
component: AnyComponent(
|
||||
AlertCallSuggestHeaderComponent()
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Calls_CallTabTitle)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.Calls_CallTabDescription))
|
||||
)
|
||||
))
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
private let iconNode: ASImageNode
|
||||
private let accentIconNode: ASImageNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
let alertController = AlertScreen(
|
||||
sharedContext: sharedContext,
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_NotNow),
|
||||
.init(title: strings.Calls_AddTab, type: .default, action: {
|
||||
let _ = updateCallListSettingsInteractively(accountManager: sharedContext.accountManager, {
|
||||
$0.withUpdatedShowTab(true)
|
||||
}).start()
|
||||
})
|
||||
]
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
||||
private final class AlertCallSuggestHeaderComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.accentIconNode = ASImageNode()
|
||||
self.accentIconNode.displaysAsynchronously = false
|
||||
self.accentIconNode.displayWithoutProcessing = true
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.accentIconNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
public static func ==(lhs: AlertCallSuggestHeaderComponent, rhs: AlertCallSuggestHeaderComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Calls_CallTabTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: strings.Calls_CallTabDescription, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/AlertIcon"), color: theme.controlBorderColor)
|
||||
self.accentIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/AlertAccentIcon"), color: theme.accentColor)
|
||||
public final class View: UIView {
|
||||
private let image = ComponentView<Empty>()
|
||||
private let accentImage = ComponentView<Empty>()
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
private var component: AlertCallSuggestHeaderComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width , 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 13.0
|
||||
|
||||
var iconSize = CGSize()
|
||||
if let icon = self.iconNode.image {
|
||||
iconSize = icon.size
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: origin.y), size: iconSize)
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
transition.updateFrame(node: self.accentIconNode, frame: iconFrame)
|
||||
origin.y += iconSize.height + 16.0
|
||||
}
|
||||
|
||||
let textSize = self.textNode.measure(size)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + iconSize.height + textSize.height + actionsHeight + 34.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
func update(component: AlertCallSuggestHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let imageSize = self.image.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: "Call List/AlertIcon", tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: 0.0), size: imageSize)
|
||||
if let imageView = self.image.view {
|
||||
if imageView.superview == nil {
|
||||
self.addSubview(imageView)
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
imageView.frame = imageFrame
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
let _ = self.accentImage.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: "Call List/AlertAccentIcon", tintColor: environment.theme.actionSheet.controlAccentColor)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let accentImageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: 0.0), size: imageSize)
|
||||
if let accentImageView = self.accentImage.view {
|
||||
if accentImageView.superview == nil {
|
||||
self.addSubview(accentImageView)
|
||||
}
|
||||
accentImageView.frame = accentImageFrame
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
func callSuggestTabController(sharedContext: SharedAccountContext) -> AlertController {
|
||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var contentNode: CallSuggestTabAlertContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Calls_AddTab, action: {
|
||||
dismissImpl?(true)
|
||||
let _ = updateCallListSettingsInteractively(accountManager: sharedContext.accountManager, {
|
||||
$0.withUpdatedShowTab(true)
|
||||
}).start()
|
||||
})]
|
||||
|
||||
contentNode = CallSuggestTabAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, actions: actions)
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
return CGSize(width: availableSize.width, height: imageSize.height)
|
||||
}
|
||||
}
|
||||
return controller
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
|||
let title: String = presentationData.strings.LiveStream_EditTitle
|
||||
let text: String = presentationData.strings.LiveStream_EditTitleText
|
||||
|
||||
let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in
|
||||
let editController = voiceChatTitleEditController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in
|
||||
guard let call = call else {
|
||||
return
|
||||
}
|
||||
|
|
@ -574,7 +574,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
|||
title = presentationData.strings.LiveStream_StartRecordingTitle
|
||||
text = presentationData.strings.LiveStream_StartRecordingTextVideo
|
||||
|
||||
let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in
|
||||
let editController = voiceChatTitleEditController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import TelegramVoip
|
|||
import TelegramAudio
|
||||
import TelegramUIPreferences
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import DeviceAccess
|
||||
import UniversalMediaPlayer
|
||||
import AccountContext
|
||||
|
|
@ -2134,12 +2135,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
}
|
||||
if case .anonymousNotAllowed = error {
|
||||
let presentationData = self.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
self.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: self.isChannel ? presentationData.strings.LiveStream_AnonymousDisabledAlertText : presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
|
||||
self.accountContext.sharedContext.mainWindow?.present(textAlertController(context: self.accountContext, title: nil, text: self.isChannel ? presentationData.strings.LiveStream_AnonymousDisabledAlertText : presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
} else if case .tooManyParticipants = error {
|
||||
let presentationData = self.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
self.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: self.isChannel ? presentationData.strings.LiveStream_ChatFullAlertText : presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
|
||||
self.accountContext.sharedContext.mainWindow?.present(textAlertController(context: self.accountContext, title: nil, text: self.isChannel ? presentationData.strings.LiveStream_ChatFullAlertText : presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
} else if case .invalidJoinAsPeer = error {
|
||||
|
|
@ -3805,7 +3806,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
break
|
||||
}
|
||||
|
||||
self.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [
|
||||
self.accountContext.sharedContext.mainWindow?.present(textAlertController(context: self.accountContext, title: nil, text: errorText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
|
||||
]), on: .root, blockInteraction: false, completion: {})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -691,7 +691,7 @@ final class VideoChatScreenComponent: Component {
|
|||
text = environment.strings.VoiceChat_EditTitleText
|
||||
}
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: groupCall.accountContext.sharedContext, account: groupCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in
|
||||
let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -469,7 +469,7 @@ extension VideoChatScreenComponent.View {
|
|||
}
|
||||
}
|
||||
|
||||
let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in
|
||||
let controller = voiceChatTitleEditController(context: currentCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let peer = self.peer, let title else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ extension VideoChatScreenComponent.View {
|
|||
} else {
|
||||
maxBioLength = 100
|
||||
}
|
||||
let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in
|
||||
let controller = voiceChatTitleEditController(context: currentCall.accountContext, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in
|
||||
guard let self, let environment = self.environment, let currentCall = self.currentCall, let bio else {
|
||||
return
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ extension VideoChatScreenComponent.View {
|
|||
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
|
||||
return
|
||||
}
|
||||
let controller = voiceChatUserNameController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in
|
||||
let controller = voiceChatUserNameController(context: currentCall.accountContext, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in
|
||||
guard let self, let environment = self.environment, let currentCall = self.currentCall, let (firstName, lastName) = firstAndLastName else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,767 +7,179 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import UrlEscaping
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertInputFieldComponent
|
||||
|
||||
private final class VoiceChatTitleEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
private let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
private let clearButton: HighlightableButtonNode
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
var textChanged: ((String) -> Void)?
|
||||
|
||||
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textInputNode.attributedText?.string ?? ""
|
||||
}
|
||||
set {
|
||||
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
if self.textInputNode.isFirstResponder() {
|
||||
self.clearButton.isHidden = newValue.isEmpty
|
||||
} else {
|
||||
self.clearButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String = "" {
|
||||
didSet {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
}
|
||||
}
|
||||
|
||||
private let maxLength: Int
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) {
|
||||
self.theme = theme
|
||||
self.maxLength = maxLength
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
|
||||
self.textInputNode = EditableTextNode()
|
||||
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.clipsToBounds = true
|
||||
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
|
||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.keyboardType = .default
|
||||
self.textInputNode.autocapitalizationType = .sentences
|
||||
self.textInputNode.returnKeyType = returnKeyType
|
||||
self.textInputNode.autocorrectionType = .default
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
self.clearButton = HighlightableButtonNode()
|
||||
self.clearButton.imageNode.displaysAsynchronously = false
|
||||
self.clearButton.imageNode.displayWithoutProcessing = true
|
||||
self.clearButton.displaysAsynchronously = false
|
||||
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.delegate = self
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
self.addSubnode(self.clearButton)
|
||||
|
||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height)))
|
||||
|
||||
if let image = self.clearButton.image(for: []) {
|
||||
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.textInputNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
self.textInputNode.resignFirstResponder()
|
||||
}
|
||||
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(editableTextNode.textView.text)
|
||||
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||
self.clearButton.isHidden = !self.placeholderNode.isHidden
|
||||
}
|
||||
|
||||
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty
|
||||
}
|
||||
|
||||
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.clearButton.isHidden = true
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
|
||||
if updatedText.count > maxLength {
|
||||
self.textInputNode.layer.addShakeAnimation()
|
||||
return false
|
||||
}
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
self.placeholderNode.isHidden = false
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
self.textInputNode.attributedText = nil
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
private final class VoiceChatTitleEditAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
let inputFieldNode: VoiceChatTitleEditInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 8
|
||||
|
||||
self.inputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength)
|
||||
self.inputFieldNode.text = value ?? ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var value: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
func voiceChatTitleEditController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController {
|
||||
var presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
if let forceTheme = forceTheme {
|
||||
func voiceChatTitleEditController(
|
||||
context: AccountContext,
|
||||
forceTheme: PresentationTheme?,
|
||||
title: String,
|
||||
text: String,
|
||||
placeholder: String,
|
||||
doneButtonTitle: String? = nil,
|
||||
value: String?,
|
||||
maxLength: Int,
|
||||
apply: @escaping (String?) -> Void
|
||||
) -> ViewController {
|
||||
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
if let forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
let strings = presentationData.strings
|
||||
|
||||
let inputState = AlertInputFieldComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: {
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = VoiceChatTitleEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
dismissImpl?(true)
|
||||
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: value,
|
||||
placeholder: placeholder,
|
||||
characterLimit: maxLength,
|
||||
hasClearButton: true,
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState,
|
||||
returnKeyAction: {
|
||||
applyImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: doneButtonTitle ?? strings.Common_Done, type: .default, action: {
|
||||
applyImpl?()
|
||||
})
|
||||
],
|
||||
updatedPresentationData: (presentationData, .single(presentationData))
|
||||
)
|
||||
applyImpl = {
|
||||
let previousValue = value ?? ""
|
||||
let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let newValue = inputState.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
apply(previousValue != newValue || value == nil ? newValue : nil)
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in
|
||||
var presentationData = presentationData
|
||||
if let forceTheme = forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller, weak contentNode] animated in
|
||||
contentNode?.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
return alertController
|
||||
}
|
||||
|
||||
private final class VoiceChatUserNameEditAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
let firstNameInputFieldNode: VoiceChatTitleEditInputFieldNode
|
||||
let lastNameInputFieldNode: VoiceChatTitleEditInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.lastNameInputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, firstNameValue: String?, lastNameValue: String?, maxLength: Int) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.firstNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: firstNamePlaceholder, maxLength: maxLength, returnKeyType: .next)
|
||||
self.firstNameInputFieldNode.text = firstNameValue ?? ""
|
||||
|
||||
self.lastNameInputFieldNode = VoiceChatTitleEditInputFieldNode(theme: ptheme, placeholder: lastNamePlaceholder, maxLength: maxLength)
|
||||
self.lastNameInputFieldNode.text = lastNameValue ?? ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
|
||||
self.addSubnode(self.firstNameInputFieldNode)
|
||||
self.addSubnode(self.lastNameInputFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
self.firstNameInputFieldNode.complete = { [weak self] in
|
||||
self?.lastNameInputFieldNode.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var firstName: String {
|
||||
return self.firstNameInputFieldNode.text
|
||||
}
|
||||
|
||||
var lastName: String {
|
||||
return self.lastNameInputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 0.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let firstInputFieldHeight = self.firstNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
transition.updateFrame(node: self.firstNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: firstInputFieldHeight))
|
||||
|
||||
origin.y += firstInputFieldHeight + spacing
|
||||
|
||||
let lastInputFieldHeight = self.lastNameInputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
transition.updateFrame(node: self.lastNameInputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: lastInputFieldHeight))
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + firstInputFieldHeight + spacing + lastInputFieldHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.firstNameInputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
if self.firstNameInputFieldNode.text.isEmpty {
|
||||
self.firstNameInputFieldNode.layer.addShakeAnimation()
|
||||
}
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
func voiceChatUserNameController(sharedContext: SharedAccountContext, account: Account, forceTheme: PresentationTheme?, title: String, firstNamePlaceholder: String, lastNamePlaceholder: String, doneButtonTitle: String? = nil, firstName: String?, lastName: String?, maxLength: Int, apply: @escaping ((String, String)?) -> Void) -> AlertController {
|
||||
var presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
if let forceTheme = forceTheme {
|
||||
func voiceChatUserNameController(
|
||||
context: AccountContext,
|
||||
forceTheme: PresentationTheme?,
|
||||
title: String,
|
||||
firstNamePlaceholder: String,
|
||||
lastNamePlaceholder: String,
|
||||
doneButtonTitle: String? = nil,
|
||||
firstName: String?,
|
||||
lastName: String?,
|
||||
maxLength: Int,
|
||||
apply: @escaping ((String, String)?) -> Void
|
||||
) -> ViewController {
|
||||
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
if let forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
let strings = presentationData.strings
|
||||
|
||||
let firstNameState = AlertInputFieldComponent.ExternalState()
|
||||
let lastNameState = AlertInputFieldComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: title)
|
||||
)
|
||||
))
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var nextImpl: (() -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "firstName",
|
||||
component: AnyComponent(
|
||||
AlertInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: firstName,
|
||||
placeholder: firstNamePlaceholder,
|
||||
characterLimit: maxLength,
|
||||
hasClearButton: true,
|
||||
returnKeyType: .next,
|
||||
isInitiallyFocused: true,
|
||||
externalState: firstNameState,
|
||||
returnKeyAction: {
|
||||
nextImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: {
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = VoiceChatUserNameEditAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, firstNamePlaceholder: firstNamePlaceholder, lastNamePlaceholder: lastNamePlaceholder, firstNameValue: firstName, lastNameValue: lastName, maxLength: maxLength)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "lastName",
|
||||
component: AnyComponent(
|
||||
AlertInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: lastName,
|
||||
placeholder: lastNamePlaceholder,
|
||||
characterLimit: maxLength,
|
||||
hasClearButton: true,
|
||||
isInitiallyFocused: false,
|
||||
externalState: lastNameState,
|
||||
returnKeyAction: {
|
||||
applyImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: doneButtonTitle ?? strings.Common_Done, type: .default, action: {
|
||||
applyImpl?()
|
||||
})
|
||||
],
|
||||
updatedPresentationData: (presentationData, .single(presentationData))
|
||||
)
|
||||
nextImpl = {
|
||||
lastNameState.activateInput()
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
|
||||
applyImpl = {
|
||||
let previousFirstName = firstName ?? ""
|
||||
let previousLastName = lastName ?? ""
|
||||
let newFirstName = contentNode.firstName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let newLastName = contentNode.lastName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let newFirstName = firstNameState.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let newLastName = lastNameState.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if newFirstName.isEmpty {
|
||||
contentNode.animateError()
|
||||
firstNameState.animateError()
|
||||
return
|
||||
}
|
||||
|
||||
dismissImpl?(true)
|
||||
|
||||
|
||||
if previousFirstName != newFirstName || previousLastName != newLastName {
|
||||
apply((newFirstName, newLastName))
|
||||
} else {
|
||||
apply(nil)
|
||||
}
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in
|
||||
var presentationData = presentationData
|
||||
if let forceTheme = forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.firstNameInputFieldNode.updateTheme(presentationData.theme)
|
||||
contentNode?.lastNameInputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller, weak contentNode] animated in
|
||||
contentNode?.firstNameInputFieldNode.deactivateInput()
|
||||
contentNode?.lastNameInputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
return alertController
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,13 +459,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
|||
}
|
||||
case let .messageMediaDice(_, value, emoticon, apiGameOutcome):
|
||||
var gameOutcome: TelegramMediaDice.GameOutcome?
|
||||
var tonAmount: Int64?
|
||||
switch apiGameOutcome {
|
||||
case let .emojiGameOutcome(seed, tonAmount):
|
||||
gameOutcome = TelegramMediaDice.GameOutcome(seed: seed.makeData(), tonAmount: tonAmount)
|
||||
case let .emojiGameOutcome(seed, stakeTonAmount, outcomeTonAmount):
|
||||
gameOutcome = TelegramMediaDice.GameOutcome(seed: seed.makeData(), tonAmount: outcomeTonAmount)
|
||||
tonAmount = stakeTonAmount
|
||||
default:
|
||||
break
|
||||
}
|
||||
return (TelegramMediaDice(emoji: emoticon, value: value, gameOutcome: gameOutcome), nil, nil, nil, nil, nil)
|
||||
return (TelegramMediaDice(emoji: emoticon, tonAmount: tonAmount, value: value, gameOutcome: gameOutcome), nil, nil, nil, nil, nil)
|
||||
case let .messageMediaStory(flags, peerId, id, _):
|
||||
let isMention = (flags & (1 << 1)) != 0
|
||||
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil, nil)
|
||||
|
|
|
|||
|
|
@ -175,6 +175,11 @@ public final class PrincipalThemeEssentialGraphics {
|
|||
public let outgoingDateAndStatusStarsIcon: UIImage
|
||||
public let mediaStarsIcon: UIImage
|
||||
public let freeStarsIcon: UIImage
|
||||
|
||||
public let incomingDateAndStatusTonIcon: UIImage
|
||||
public let outgoingDateAndStatusTonIcon: UIImage
|
||||
public let mediaTonIcon: UIImage
|
||||
public let freeTonIcon: UIImage
|
||||
|
||||
public let incomingDateAndStatusPinnedIcon: UIImage
|
||||
public let outgoingDateAndStatusPinnedIcon: UIImage
|
||||
|
|
@ -369,6 +374,12 @@ public final class PrincipalThemeEssentialGraphics {
|
|||
self.outgoingDateAndStatusStarsIcon = generateTintedImage(image: starsImage, color: theme.message.outgoing.secondaryTextColor)!
|
||||
self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)!
|
||||
self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)!
|
||||
|
||||
let tonImage = generateScaledImage(image: UIImage(bundleImageName: "Ads/TonMedium"), size: CGSize(width: 12.0, height: 12.0), opaque: false)!
|
||||
self.incomingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.incoming.secondaryTextColor)!
|
||||
self.outgoingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.outgoing.secondaryTextColor)!
|
||||
self.mediaTonIcon = generateTintedImage(image: tonImage, color: .white)!
|
||||
self.freeTonIcon = generateTintedImage(image: tonImage, color: serviceColor.primaryText)!
|
||||
|
||||
let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")!
|
||||
self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)!
|
||||
|
|
@ -497,6 +508,12 @@ public final class PrincipalThemeEssentialGraphics {
|
|||
self.mediaStarsIcon = generateTintedImage(image: starsImage, color: .white)!
|
||||
self.freeStarsIcon = generateTintedImage(image: starsImage, color: serviceColor.primaryText)!
|
||||
|
||||
let tonImage = generateScaledImage(image: UIImage(bundleImageName: "Ads/TonMedium"), size: CGSize(width: 12.0, height: 12.0), opaque: false)!
|
||||
self.incomingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.incoming.secondaryTextColor)!
|
||||
self.outgoingDateAndStatusTonIcon = generateTintedImage(image: tonImage, color: theme.message.outgoing.secondaryTextColor)!
|
||||
self.mediaTonIcon = generateTintedImage(image: tonImage, color: .white)!
|
||||
self.freeTonIcon = generateTintedImage(image: tonImage, color: serviceColor.primaryText)!
|
||||
|
||||
let pinnedImage = UIImage(bundleImageName: "Chat/Message/Pinned")!
|
||||
self.incomingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.incoming.secondaryTextColor)!
|
||||
self.outgoingDateAndStatusPinnedIcon = generateTintedImage(image: pinnedImage, color: theme.message.outgoing.secondaryTextColor)!
|
||||
|
|
|
|||
|
|
@ -308,6 +308,8 @@ public enum PresentationResourceKey: Int32 {
|
|||
case chatFreeShareButtonIcon
|
||||
case chatFreeCloseButtonIcon
|
||||
case chatFreeMoreButtonIcon
|
||||
case chatFreeExpandButtonIcon
|
||||
case chatFreeCollapseButtonIcon
|
||||
|
||||
case chatKeyboardActionButtonMessageIcon
|
||||
case chatKeyboardActionButtonLinkIcon
|
||||
|
|
|
|||
|
|
@ -1174,6 +1174,18 @@ public struct PresentationResourcesChat {
|
|||
})
|
||||
}
|
||||
|
||||
public static func chatFreeExpandButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatFreeExpandButtonIcon.rawValue, { _ in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpandIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper))
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatFreeCollapseButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatFreeCollapseButtonIcon.rawValue, { _ in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CollapseIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper))
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatKeyboardActionButtonMessageIconImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatKeyboardActionButtonMessageIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: theme.chat.inputButtonPanel.buttonTextColor)
|
||||
|
|
|
|||
|
|
@ -1765,10 +1765,19 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||
} else if let dice = media as? TelegramMediaDice, let gameOutcome = dice.gameOutcome {
|
||||
if let value = dice.value, value > 1 {
|
||||
if let value = dice.value {
|
||||
//TODO:localize
|
||||
let value = formatTonAmountText(gameOutcome.tonAmount, dateTimeFormat: dateTimeFormat)
|
||||
let attributedText = NSMutableAttributedString(string: "You won $\(value)", font: titleFont, textColor: primaryTextColor)
|
||||
|
||||
let rawString: String
|
||||
if value == 1, let tonAmount = dice.tonAmount {
|
||||
let value = formatTonAmountText(tonAmount, dateTimeFormat: dateTimeFormat)
|
||||
rawString = "You lost $\(value)"
|
||||
} else {
|
||||
let value = formatTonAmountText(gameOutcome.tonAmount, dateTimeFormat: dateTimeFormat)
|
||||
rawString = "You won $\(value)"
|
||||
}
|
||||
|
||||
let attributedText = NSMutableAttributedString(string: rawString, font: titleFont, textColor: primaryTextColor)
|
||||
if let range = attributedText.string.range(of: "$") {
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: true)), range: NSRange(range, in: attributedText.string))
|
||||
attributedText.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedText.string))
|
||||
|
|
|
|||
|
|
@ -511,6 +511,8 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Gifts/GiftDemoScreen",
|
||||
"//submodules/TelegramUI/Components/EmojiGameStakeScreen",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController",
|
||||
"//submodules/TelegramUI/Components/CocoonInfoScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
|
|
|||
|
|
@ -1211,8 +1211,7 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
return
|
||||
}
|
||||
if !isEnabled {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [
|
||||
self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [
|
||||
TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {
|
||||
})
|
||||
]), in: .window(.root))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertCheckComponent",
|
||||
module_name = "AlertCheckComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/CheckComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
import CheckComponent
|
||||
import TextFormat
|
||||
import Markdown
|
||||
|
||||
public final class AlertCheckComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var value: Bool
|
||||
fileprivate var valuePromise = Promise<Bool>()
|
||||
public var valueSignal: Signal<Bool, NoError>
|
||||
|
||||
public init() {
|
||||
self.value = false
|
||||
self.valueSignal = self.valuePromise.get()
|
||||
}
|
||||
}
|
||||
|
||||
let title: String
|
||||
let initialValue: Bool
|
||||
let externalState: ExternalState
|
||||
let linkAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
initialValue: Bool,
|
||||
externalState: ExternalState,
|
||||
linkAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.initialValue = initialValue
|
||||
self.externalState = externalState
|
||||
self.linkAction = linkAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertCheckComponent, rhs: AlertCheckComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertCheckComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isUpdating = false
|
||||
|
||||
private var valuePromise = ValuePromise<Bool>(false)
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
func findTextView(view: UIView?) -> ImmediateTextView? {
|
||||
if let view {
|
||||
if let view = view as? ImmediateTextView {
|
||||
return view
|
||||
}
|
||||
for view in view.subviews {
|
||||
if let result = findTextView(view: view) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let result = super.hitTest(point, with: event)
|
||||
if let textView = findTextView(view: result) {
|
||||
if let (_, attributes) = textView.attributesAtPoint(self.convert(point, to: textView)) {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil {
|
||||
return textView
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func update(component: AlertCheckComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
if self.component == nil {
|
||||
component.externalState.value = component.initialValue
|
||||
component.externalState.valuePromise.set(self.valuePromise.get())
|
||||
}
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let checkTheme = CheckComponent.Theme(
|
||||
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
|
||||
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
borderColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.15),
|
||||
overlayBorder: false,
|
||||
hasInset: false,
|
||||
hasShadow: false
|
||||
)
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let textColor = environment.theme.actionSheet.primaryTextColor
|
||||
let linkColor = environment.theme.actionSheet.controlAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
|
||||
theme: checkTheme,
|
||||
size: CGSize(width: 18.0, height: 18.0),
|
||||
selected: component.externalState.value
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(text: component.title, attributes: markdownAttributes),
|
||||
maximumNumberOfLines: 2,
|
||||
highlightColor: linkColor.withAlphaComponent(0.1),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
component.linkAction?()
|
||||
}
|
||||
}
|
||||
)))
|
||||
], spacing: 10.0)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.externalState.value = !component.externalState.value
|
||||
self.valuePromise.set(component.externalState.value)
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: false
|
||||
)),
|
||||
environment: {
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width + 20.0, height: 1000.0)
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: 7.0), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: buttonSize.height + 7.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertInputFieldComponent",
|
||||
module_name = "AlertInputFieldComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import MultilineTextComponent
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import PlainButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class AlertInputFieldComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var value: String = ""
|
||||
public fileprivate(set) var animateError: () -> Void = {}
|
||||
public fileprivate(set) var activateInput: () -> Void = {}
|
||||
fileprivate let valuePromise = ValuePromise<String>("")
|
||||
public var valueSignal: Signal<String, NoError> {
|
||||
return self.valuePromise.get()
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let initialValue: String?
|
||||
let placeholder: String
|
||||
let characterLimit: Int?
|
||||
let hasClearButton: Bool
|
||||
let isSecureTextEntry: Bool
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let keyboardType: UIKeyboardType
|
||||
let autocapitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let isInitiallyFocused: Bool
|
||||
let externalState: ExternalState
|
||||
let shouldChangeText: ((String) -> Bool)?
|
||||
let returnKeyAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
initialValue: String? = nil,
|
||||
placeholder: String,
|
||||
characterLimit: Int? = nil,
|
||||
hasClearButton: Bool = false,
|
||||
isSecureTextEntry: Bool = false,
|
||||
returnKeyType: UIReturnKeyType = .done,
|
||||
keyboardType: UIKeyboardType = .default,
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
isInitiallyFocused: Bool = false,
|
||||
externalState: ExternalState,
|
||||
shouldChangeText: ((String) -> Bool)? = nil,
|
||||
returnKeyAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.initialValue = initialValue
|
||||
self.placeholder = placeholder
|
||||
self.characterLimit = characterLimit
|
||||
self.hasClearButton = hasClearButton
|
||||
self.isSecureTextEntry = isSecureTextEntry
|
||||
self.returnKeyType = returnKeyType
|
||||
self.keyboardType = keyboardType
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.isInitiallyFocused = isInitiallyFocused
|
||||
self.externalState = externalState
|
||||
self.shouldChangeText = shouldChangeText
|
||||
self.returnKeyAction = returnKeyAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.initialValue != rhs.initialValue {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.characterLimit != rhs.characterLimit {
|
||||
return false
|
||||
}
|
||||
if lhs.hasClearButton != rhs.hasClearButton {
|
||||
return false
|
||||
}
|
||||
if lhs.isSecureTextEntry != rhs.isSecureTextEntry {
|
||||
return false
|
||||
}
|
||||
if lhs.returnKeyType != rhs.returnKeyType {
|
||||
return false
|
||||
}
|
||||
if lhs.keyboardType != rhs.keyboardType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocorrectionType != rhs.autocorrectionType {
|
||||
return false
|
||||
}
|
||||
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class TextField: UITextField {
|
||||
var sideInset: CGFloat = 0.0
|
||||
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
|
||||
}
|
||||
|
||||
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UITextFieldDelegate {
|
||||
private let background = ComponentView<Empty>()
|
||||
private let textField = TextField()
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
private let clearButton = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertInputFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isUpdating = false
|
||||
|
||||
var currentText: String {
|
||||
return self.textField.text ?? ""
|
||||
}
|
||||
|
||||
private var clearOnce: Bool = false
|
||||
|
||||
func activateInput() {
|
||||
self.textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
if let component = self.component, component.isInitiallyFocused {
|
||||
self.clearOnce = true
|
||||
}
|
||||
self.textField.layer.addShakeAnimation()
|
||||
|
||||
HapticFeedback().error()
|
||||
}
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
self.component?.returnKeyAction?()
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func textDidChange() {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
self.clearButton.view?.isHidden = self.currentText.isEmpty
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
self.clearButton.view?.isHidden = true
|
||||
}
|
||||
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard let component = self.component else {
|
||||
return true
|
||||
}
|
||||
|
||||
if self.clearOnce {
|
||||
self.clearOnce = false
|
||||
if range.length > string.count {
|
||||
textField.text = ""
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
if let shouldChangeText = component.shouldChangeText {
|
||||
return shouldChangeText(updatedText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func setText(text: String) {
|
||||
self.textField.text = text
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
var resetText: String?
|
||||
if self.component == nil {
|
||||
resetText = component.initialValue
|
||||
component.externalState.animateError = { [weak self] in
|
||||
self?.animateError()
|
||||
}
|
||||
component.externalState.activateInput = { [weak self] in
|
||||
self?.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let topInset: CGFloat = 15.0
|
||||
|
||||
if self.textField.superview == nil {
|
||||
self.addSubview(self.textField)
|
||||
self.textField.delegate = self
|
||||
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
|
||||
}
|
||||
if self.textField.autocapitalizationType != component.autocapitalizationType {
|
||||
self.textField.autocapitalizationType = component.autocapitalizationType
|
||||
}
|
||||
if self.textField.autocorrectionType != component.autocorrectionType {
|
||||
self.textField.autocorrectionType = component.autocorrectionType
|
||||
}
|
||||
if self.textField.isSecureTextEntry != component.isSecureTextEntry {
|
||||
self.textField.isSecureTextEntry = component.isSecureTextEntry
|
||||
}
|
||||
if self.textField.returnKeyType != component.returnKeyType {
|
||||
self.textField.returnKeyType = component.returnKeyType
|
||||
}
|
||||
self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light
|
||||
if let resetText {
|
||||
self.textField.text = resetText
|
||||
}
|
||||
|
||||
self.textField.font = Font.regular(17.0)
|
||||
self.textField.textColor = environment.theme.actionSheet.primaryTextColor
|
||||
self.textField.tintColor = environment.theme.actionSheet.controlAccentColor
|
||||
self.textField.sideInset = 16.0
|
||||
|
||||
let backgroundPadding: CGFloat = 14.0
|
||||
let size = CGSize(width: availableSize.width, height: 50.0)
|
||||
|
||||
let backgroundSize = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
|
||||
let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0)
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize)
|
||||
transition.setFrame(view: self.textField, frame: textFieldFrame)
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(
|
||||
string: component.placeholder,
|
||||
font: Font.regular(17.0),
|
||||
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
|
||||
)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 50.0)
|
||||
)
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if placeholderView.superview == nil {
|
||||
placeholderView.isUserInteractionEnabled = false
|
||||
self.addSubview(placeholderView)
|
||||
}
|
||||
placeholderView.frame = placeholderFrame
|
||||
placeholderView.isHidden = !self.currentText.isEmpty
|
||||
}
|
||||
|
||||
if component.hasClearButton {
|
||||
let clearButtonSize = self.clearButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Components/Search Bar/Clear",
|
||||
tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setText(text: "")
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
if let clearButtonView = self.clearButton.view {
|
||||
if clearButtonView.superview == nil {
|
||||
self.addSubview(clearButtonView)
|
||||
}
|
||||
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize))
|
||||
clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder
|
||||
}
|
||||
} else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil {
|
||||
clearButtonView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if isFirstTime && component.isInitiallyFocused {
|
||||
self.activateInput()
|
||||
}
|
||||
|
||||
component.externalState.value = self.currentText
|
||||
component.externalState.valuePromise.set(self.currentText)
|
||||
|
||||
return CGSize(width: availableSize.width, height: size.height + topInset)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertMultilineInputFieldComponent",
|
||||
module_name = "AlertMultilineInputFieldComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import TextFieldComponent
|
||||
import MultilineTextComponent
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
|
||||
public final class AlertMultilineInputFieldComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var value: NSAttributedString = NSAttributedString()
|
||||
public fileprivate(set) var animateError: () -> Void = {}
|
||||
public fileprivate(set) var activateInput: () -> Void = {}
|
||||
fileprivate let valuePromise = ValuePromise<NSAttributedString>(NSAttributedString())
|
||||
public var valueSignal: Signal<NSAttributedString, NoError> {
|
||||
return self.valuePromise.get()
|
||||
}
|
||||
|
||||
public var textAndEntities: (String, [MessageTextEntity]) {
|
||||
let text = self.value.string
|
||||
let entities = generateChatInputTextEntities(self.value)
|
||||
return (text, entities)
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FormatMenuAvailability: Equatable {
|
||||
public enum Action: CaseIterable {
|
||||
case bold
|
||||
case italic
|
||||
case monospace
|
||||
case link
|
||||
case strikethrough
|
||||
case underline
|
||||
case spoiler
|
||||
case quote
|
||||
case code
|
||||
|
||||
public static var all: [Action] = [
|
||||
.bold,
|
||||
.italic,
|
||||
.monospace,
|
||||
.link,
|
||||
.strikethrough,
|
||||
.underline,
|
||||
.spoiler,
|
||||
.quote,
|
||||
.code
|
||||
]
|
||||
|
||||
var textFieldValue: TextFieldComponent.FormatMenuAvailability.Action {
|
||||
switch self {
|
||||
case .bold:
|
||||
return .bold
|
||||
case .italic:
|
||||
return .italic
|
||||
case .monospace:
|
||||
return .monospace
|
||||
case .link:
|
||||
return .link
|
||||
case .strikethrough:
|
||||
return .strikethrough
|
||||
case .underline:
|
||||
return .underline
|
||||
case .spoiler:
|
||||
return .spoiler
|
||||
case .quote:
|
||||
return .quote
|
||||
case .code:
|
||||
return .code
|
||||
}
|
||||
}
|
||||
}
|
||||
case available([Action])
|
||||
case none
|
||||
|
||||
var textFieldValue: TextFieldComponent.FormatMenuAvailability {
|
||||
switch self {
|
||||
case let .available(actions):
|
||||
return .available(actions.map { $0.textFieldValue })
|
||||
case .none:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum EmptyLineHandling {
|
||||
case allowed
|
||||
case oneConsecutive
|
||||
case notAllowed
|
||||
|
||||
var textFieldValue: TextFieldComponent.EmptyLineHandling {
|
||||
switch self {
|
||||
case .allowed:
|
||||
return .allowed
|
||||
case .oneConsecutive:
|
||||
return .oneConsecutive
|
||||
case .notAllowed:
|
||||
return .notAllowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let initialValue: NSAttributedString?
|
||||
let placeholder: String
|
||||
let prefix: NSAttributedString?
|
||||
let characterLimit: Int?
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let keyboardType: UIKeyboardType
|
||||
let autocapitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let formatMenuAvailability: FormatMenuAvailability
|
||||
let emptyLineHandling: EmptyLineHandling
|
||||
let isInitiallyFocused: Bool
|
||||
let externalState: ExternalState
|
||||
let present: (ViewController) -> Void
|
||||
let returnKeyAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
initialValue: NSAttributedString? = nil,
|
||||
placeholder: String,
|
||||
prefix: NSAttributedString? = nil,
|
||||
characterLimit: Int? = nil,
|
||||
returnKeyType: UIReturnKeyType = .default,
|
||||
keyboardType: UIKeyboardType = .default,
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
formatMenuAvailability: FormatMenuAvailability = .none,
|
||||
emptyLineHandling: EmptyLineHandling = .allowed,
|
||||
isInitiallyFocused: Bool = false,
|
||||
externalState: ExternalState,
|
||||
present: @escaping (ViewController) -> Void = { _ in },
|
||||
returnKeyAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.initialValue = initialValue
|
||||
self.placeholder = placeholder
|
||||
self.prefix = prefix
|
||||
self.characterLimit = characterLimit
|
||||
self.returnKeyType = returnKeyType
|
||||
self.keyboardType = keyboardType
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.formatMenuAvailability = formatMenuAvailability
|
||||
self.emptyLineHandling = emptyLineHandling
|
||||
self.isInitiallyFocused = isInitiallyFocused
|
||||
self.externalState = externalState
|
||||
self.present = present
|
||||
self.returnKeyAction = returnKeyAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertMultilineInputFieldComponent, rhs: AlertMultilineInputFieldComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.initialValue != rhs.initialValue {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.prefix != rhs.prefix {
|
||||
return false
|
||||
}
|
||||
if lhs.returnKeyType != rhs.returnKeyType {
|
||||
return false
|
||||
}
|
||||
if lhs.characterLimit != rhs.characterLimit {
|
||||
return false
|
||||
}
|
||||
if lhs.keyboardType != rhs.keyboardType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocorrectionType != rhs.autocorrectionType {
|
||||
return false
|
||||
}
|
||||
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
||||
return false
|
||||
}
|
||||
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
||||
return false
|
||||
}
|
||||
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let background = ComponentView<Empty>()
|
||||
private let textField = ComponentView<Empty>()
|
||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertMultilineInputFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func activateInput() {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
if let textFieldView = self.textField.view {
|
||||
textFieldView.layer.addShakeAnimation()
|
||||
}
|
||||
HapticFeedback().error()
|
||||
}
|
||||
|
||||
func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
var resetText: NSAttributedString?
|
||||
if self.component == nil {
|
||||
resetText = component.initialValue
|
||||
component.externalState.animateError = { [weak self] in
|
||||
self?.animateError()
|
||||
}
|
||||
component.externalState.activateInput = { [weak self] in
|
||||
self?.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let topInset: CGFloat = 15.0
|
||||
let horizontalInset: CGFloat = 4.0
|
||||
let verticalInset: CGFloat = 11.0 - UIScreenPixel
|
||||
|
||||
let textFieldSize = self.textField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TextFieldComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
externalState: self.textFieldExternalState,
|
||||
fontSize: 17.0,
|
||||
textColor: environment.theme.actionSheet.primaryTextColor,
|
||||
accentColor: environment.theme.actionSheet.controlAccentColor,
|
||||
insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0),
|
||||
hideKeyboard: false,
|
||||
customInputView: nil,
|
||||
resetText: resetText,
|
||||
isOneLineWhenUnfocused: false,
|
||||
characterLimit: component.characterLimit,
|
||||
emptyLineHandling: component.emptyLineHandling.textFieldValue,
|
||||
formatMenuAvailability: component.formatMenuAvailability.textFieldValue,
|
||||
returnKeyType: component.returnKeyType,
|
||||
keyboardType: component.keyboardType,
|
||||
autocapitalizationType: component.autocapitalizationType,
|
||||
autocorrectionType: component.autocorrectionType,
|
||||
lockedFormatAction: {
|
||||
},
|
||||
present: { [weak self] c in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.present(c)
|
||||
},
|
||||
paste: { _ in
|
||||
},
|
||||
returnKeyAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.returnKeyAction?()
|
||||
},
|
||||
backspaceKeyAction: {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + horizontalInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
component.externalState.value = self.textFieldExternalState.text
|
||||
component.externalState.valuePromise.set(component.externalState.value)
|
||||
|
||||
let backgroundPadding: CGFloat = 14.0
|
||||
let size = CGSize(width: availableSize.width, height: max(50.0, floor(textFieldSize.height + verticalInset * 2.0)))
|
||||
|
||||
let backgroundSize = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textFieldSize.width) / 2.0), y: topInset + 11.0 - UIScreenPixel), size: textFieldSize)
|
||||
if let textFieldView = self.textField.view {
|
||||
if textFieldView.superview == nil {
|
||||
self.addSubview(textFieldView)
|
||||
self.textField.parentState = state
|
||||
}
|
||||
transition.setFrame(view: textFieldView, frame: textFieldFrame)
|
||||
}
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(
|
||||
string: component.placeholder,
|
||||
font: Font.regular(17.0),
|
||||
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
|
||||
)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 50.0)
|
||||
)
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if placeholderView.superview == nil {
|
||||
placeholderView.isUserInteractionEnabled = false
|
||||
self.addSubview(placeholderView)
|
||||
}
|
||||
placeholderView.frame = placeholderFrame
|
||||
placeholderView.isHidden = self.textFieldExternalState.hasText
|
||||
}
|
||||
|
||||
if isFirstTime && component.isInitiallyFocused {
|
||||
self.activateInput()
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: size.height + topInset)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertTableComponent",
|
||||
module_name = "AlertTableComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/TableComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import TableComponent
|
||||
|
||||
public final class AlertTableComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
let items: [TableComponent.Item]
|
||||
|
||||
public init(
|
||||
items: [TableComponent.Item]
|
||||
) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTableComponent, rhs: AlertTableComponent) -> Bool {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let table = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTableComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let tableSize = self.table.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
TableComponent(
|
||||
theme: environment.theme,
|
||||
items: component.items,
|
||||
semiTransparent: true
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + 20.0, height: availableSize.height)
|
||||
)
|
||||
let tableFrame = CGRect(origin: CGPoint(x: -10.0, y: 5.0), size: tableSize)
|
||||
if let tableView = self.table.view {
|
||||
if tableView.superview == nil {
|
||||
self.addSubview(tableView)
|
||||
}
|
||||
transition.setFrame(view: tableView, frame: tableFrame)
|
||||
}
|
||||
return CGSize(width: availableSize.width, height: tableSize.height + 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertTransferHeaderComponent",
|
||||
module_name = "AlertTransferHeaderComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class AlertTransferHeaderComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public enum IconType {
|
||||
case transfer
|
||||
case take
|
||||
}
|
||||
|
||||
let fromComponent: AnyComponentWithIdentity<Empty>
|
||||
let toComponent: AnyComponentWithIdentity<Empty>
|
||||
let type: IconType
|
||||
|
||||
public init(
|
||||
fromComponent: AnyComponentWithIdentity<Empty>,
|
||||
toComponent: AnyComponentWithIdentity<Empty>,
|
||||
type: IconType
|
||||
) {
|
||||
self.fromComponent = fromComponent
|
||||
self.toComponent = toComponent
|
||||
self.type = type
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTransferHeaderComponent, rhs: AlertTransferHeaderComponent) -> Bool {
|
||||
if lhs.fromComponent != rhs.fromComponent {
|
||||
return false
|
||||
}
|
||||
if lhs.toComponent != rhs.toComponent {
|
||||
return false
|
||||
}
|
||||
if lhs.type != rhs.type {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let from = ComponentView<Empty>()
|
||||
private let to = ComponentView<Empty>()
|
||||
private let arrow = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTransferHeaderComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertTransferHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let size: CGSize
|
||||
let iconName: String
|
||||
switch component.type {
|
||||
case .transfer:
|
||||
iconName = "Peer Info/AlertArrow"
|
||||
size = CGSize(width: 148.0, height: 60.0)
|
||||
case .take:
|
||||
iconName = "Media Editor/CutoutUndo"
|
||||
size = CGSize(width: 154.0, height: 60.0)
|
||||
}
|
||||
let sideInset = floorToScreenPixels((availableSize.width - size.width) / 2.0)
|
||||
|
||||
let fromSize = self.from.update(
|
||||
transition: transition,
|
||||
component: component.fromComponent.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 60.0, height: 60.0)
|
||||
)
|
||||
let fromFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: fromSize)
|
||||
if let fromView = self.from.view {
|
||||
if fromView.superview == nil {
|
||||
self.addSubview(fromView)
|
||||
}
|
||||
transition.setFrame(view: fromView, frame: fromFrame)
|
||||
}
|
||||
|
||||
let arrowSize = self.arrow.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: iconName, tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - arrowSize.width) / 2.0), y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize)
|
||||
if let arrowView = self.arrow.view {
|
||||
if arrowView.superview == nil {
|
||||
self.addSubview(arrowView)
|
||||
}
|
||||
transition.setFrame(view: arrowView, frame: arrowFrame)
|
||||
}
|
||||
|
||||
let toSize = self.to.update(
|
||||
transition: transition,
|
||||
component: component.toComponent.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 60.0, height: 60.0)
|
||||
)
|
||||
let toFrame = CGRect(origin: CGPoint(x: availableSize.width - toSize.width - sideInset, y: 0.0), size: toSize)
|
||||
if let toView = self.to.view {
|
||||
if toView.superview == nil {
|
||||
self.addSubview(toView)
|
||||
}
|
||||
transition.setFrame(view: toView, frame: toFrame)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: size.height + 11.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,12 +15,14 @@ swift_library(
|
|||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/ActivityIndicatorComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -33,17 +33,20 @@ final class AlertActionComponent: Component {
|
|||
let theme: Theme
|
||||
let title: String
|
||||
let isHighlighted: Bool
|
||||
let isEnabled: Signal<Bool, NoError>
|
||||
let progress: Signal<Bool, NoError>
|
||||
|
||||
init(
|
||||
theme: Theme,
|
||||
title: String,
|
||||
isHighlighted: Bool,
|
||||
isEnabled: Signal<Bool, NoError>,
|
||||
progress: Signal<Bool, NoError>
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.isHighlighted = isHighlighted
|
||||
self.isEnabled = isEnabled
|
||||
self.progress = progress
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +71,9 @@ final class AlertActionComponent: Component {
|
|||
private var component: AlertActionComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isEnabledDisposable: Disposable?
|
||||
private var isEnabled = true
|
||||
|
||||
private var progressDisposable: Disposable?
|
||||
private var hasProgress = false
|
||||
|
||||
|
|
@ -84,12 +90,28 @@ final class AlertActionComponent: Component {
|
|||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.isEnabledDisposable?.dispose()
|
||||
self.progressDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(component: AlertActionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
if self.component == nil {
|
||||
self.isEnabledDisposable = (component.isEnabled
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isEnabled in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isEnabled = isEnabled
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
})
|
||||
|
||||
self.progressDisposable = (component.progress
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
|
||||
guard let self else {
|
||||
|
|
@ -113,7 +135,7 @@ final class AlertActionComponent: Component {
|
|||
|
||||
let titlePadding: CGFloat = 16.0
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(attributedString),
|
||||
horizontalAlignment: .center,
|
||||
|
|
@ -139,17 +161,14 @@ final class AlertActionComponent: Component {
|
|||
activity = ComponentView()
|
||||
self.activity = activity
|
||||
}
|
||||
let activitySize = activity.update(
|
||||
let activitySize = CGSize(width: 18.0, height: 18.0)
|
||||
let _ = activity.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: activitySize
|
||||
)
|
||||
if let activityView = activity.view {
|
||||
if activityView.superview == nil {
|
||||
self.addSubview(activityView)
|
||||
transition.animateAlpha(view: activityView, from: 0.0, to: 1.0)
|
||||
}
|
||||
activityView.bounds = CGRect(origin: .zero, size: activitySize)
|
||||
}
|
||||
} else if let activity = self.activity {
|
||||
|
|
@ -161,8 +180,15 @@ final class AlertActionComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
let buttonAlpha: CGFloat
|
||||
if self.isEnabled {
|
||||
buttonAlpha = component.isHighlighted ? 0.35 : 1.0
|
||||
} else {
|
||||
buttonAlpha = 0.2
|
||||
}
|
||||
|
||||
transition.setBackgroundColor(view: self.backgroundView, color: component.theme.background)
|
||||
transition.setAlpha(view: self.backgroundView, alpha: component.isHighlighted ? 0.35 : 1.0)
|
||||
transition.setAlpha(view: self.backgroundView, alpha: buttonAlpha)
|
||||
self.backgroundView.layer.cornerRadius = availableSize.height * 0.5
|
||||
|
||||
return CGSize(width: titleSize.width + titlePadding * 2.0, height: availableSize.height)
|
||||
|
|
@ -178,9 +204,16 @@ final class AlertActionComponent: Component {
|
|||
}
|
||||
|
||||
if let activityView = self.activity?.view {
|
||||
var activityTransition = transition
|
||||
if activityView.superview == nil {
|
||||
self.addSubview(activityView)
|
||||
transition.animateAlpha(view: activityView, from: 0.0, to: 1.0)
|
||||
activityTransition = .immediate
|
||||
}
|
||||
let activitySize = activityView.bounds.size
|
||||
let activityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - activitySize.width) / 2.0), y: floorToScreenPixels((size.height - activitySize.height) / 2.0)), size: activitySize)
|
||||
transition.setFrame(view: activityView, frame: activityFrame)
|
||||
activityTransition.setPosition(view: activityView, position: activityFrame.center)
|
||||
activityView.transform = CGAffineTransformMakeScale(0.7, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,15 +13,23 @@ import GlassBackgroundComponent
|
|||
|
||||
public final class AlertComponentEnvironment: Equatable {
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
|
||||
public init(theme: PresentationTheme) {
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertComponentEnvironment, rhs: AlertComponentEnvironment) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -30,14 +38,14 @@ private final class AlertScreenComponent: Component {
|
|||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let configuration: AlertScreen.Configuration
|
||||
let content: [AnyComponentWithIdentity<AlertComponentEnvironment>]
|
||||
let actions: [AlertScreen.Action]
|
||||
let content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>
|
||||
let actions: Signal<[AlertScreen.Action], NoError>
|
||||
let ready: Promise<Bool>
|
||||
|
||||
init(
|
||||
configuration: AlertScreen.Configuration,
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [AlertScreen.Action],
|
||||
content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
|
||||
actions: Signal<[AlertScreen.Action], NoError>,
|
||||
ready: Promise<Bool>
|
||||
) {
|
||||
self.configuration = configuration
|
||||
|
|
@ -64,8 +72,12 @@ private final class AlertScreenComponent: Component {
|
|||
private let containerView = GlassBackgroundContainerView()
|
||||
private let backgroundView = GlassBackgroundView()
|
||||
|
||||
private var content: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
|
||||
private var actions: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
|
||||
private var disposable: Disposable?
|
||||
private var content: [AnyComponentWithIdentity<AlertComponentEnvironment>]?
|
||||
private var actions: [AlertScreen.Action]?
|
||||
|
||||
private var contentItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
|
||||
private var actionItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
|
||||
|
||||
private var highlightedAction: AnyHashable?
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
|
@ -114,10 +126,14 @@ private final class AlertScreenComponent: Component {
|
|||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is ActionSelectionGestureRecognizer {
|
||||
let location = gestureRecognizer.location(in: self.backgroundView)
|
||||
for (_, action) in self.actions {
|
||||
for (_, action) in self.actionItems {
|
||||
if let actionView = action.view, actionView.frame.contains(location) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -133,7 +149,7 @@ private final class AlertScreenComponent: Component {
|
|||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
var highlightedActionId: AnyHashable?
|
||||
for (actionId, action) in self.actions {
|
||||
for (actionId, action) in self.actionItems {
|
||||
if let actionView = action.view, actionView.frame.contains(location) {
|
||||
highlightedActionId = actionId
|
||||
break
|
||||
|
|
@ -235,24 +251,24 @@ private final class AlertScreenComponent: Component {
|
|||
}
|
||||
|
||||
func updateActionHighlight(previous: Bool) {
|
||||
guard let component = self.component else {
|
||||
guard let actions = self.actions else {
|
||||
return
|
||||
}
|
||||
guard let highlightedAction = self.highlightedAction else {
|
||||
if let action = component.actions.first(where: { $0.type == .default }) {
|
||||
if let action = actions.first(where: { $0.type == .default }) {
|
||||
self.highlightedAction = action.id
|
||||
} else if let action = component.actions.first(where: { $0.type == .defaultDestructive }) {
|
||||
} else if let action = actions.first(where: { $0.type == .defaultDestructive }) {
|
||||
self.highlightedAction = action.id
|
||||
} else if case .verticalReversed = self.effectiveActionLayout, let action = component.actions.last {
|
||||
} else if case .verticalReversed = self.effectiveActionLayout, let action = actions.last {
|
||||
self.highlightedAction = action.id
|
||||
} else if let action = component.actions.first {
|
||||
} else if let action = actions.first {
|
||||
self.highlightedAction = action.id
|
||||
}
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
return
|
||||
}
|
||||
|
||||
let sequence = previous ? component.actions.reversed() : component.actions
|
||||
let sequence = previous ? actions.reversed() : actions
|
||||
var selectNext = false
|
||||
var newHighlightedAction: AnyHashable?
|
||||
|
||||
|
|
@ -273,13 +289,13 @@ private final class AlertScreenComponent: Component {
|
|||
}
|
||||
|
||||
func performHighlightedAction() {
|
||||
guard let component = self.component else {
|
||||
guard let actions = self.actions else {
|
||||
return
|
||||
}
|
||||
guard let highlightedAction = self.highlightedAction else {
|
||||
return
|
||||
}
|
||||
guard let action = component.actions.first(where: { AnyHashable($0.id) == highlightedAction }) else {
|
||||
guard let action = actions.first(where: { AnyHashable($0.id) == highlightedAction }) else {
|
||||
return
|
||||
}
|
||||
action.action()
|
||||
|
|
@ -299,8 +315,23 @@ private final class AlertScreenComponent: Component {
|
|||
self.state = state
|
||||
|
||||
if self.component == nil {
|
||||
|
||||
self.disposable = (combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
component.content,
|
||||
component.actions
|
||||
) |> deliverOnMainQueue).start(next: { [weak self] content, actions in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.content = content
|
||||
self.actions = actions
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
||||
var alertHeight: CGFloat = 0.0
|
||||
|
|
@ -314,44 +345,47 @@ private final class AlertScreenComponent: Component {
|
|||
let fullWidthActionSize = CGSize(width: alertWidth - actionSideInset * 2.0, height: AlertActionComponent.actionHeight)
|
||||
let halfWidthActionSize = CGSize(width: (alertWidth - actionSideInset * 2.0 - actionSpacing) / 2.0, height: AlertActionComponent.actionHeight)
|
||||
|
||||
let alertEnvironment = AlertComponentEnvironment(theme: environment.theme)
|
||||
let alertEnvironment = AlertComponentEnvironment(theme: environment.theme, strings: environment.strings)
|
||||
|
||||
var contentOriginY: CGFloat = 0.0
|
||||
var validContentIds: Set<AnyHashable> = Set()
|
||||
for content in component.content {
|
||||
if contentOriginY.isZero {
|
||||
contentOriginY += contentTopInset
|
||||
} else {
|
||||
contentOriginY += contentSpacing
|
||||
}
|
||||
validContentIds.insert(content.id)
|
||||
|
||||
let item: ComponentView<AlertComponentEnvironment>
|
||||
var itemTransition = transition
|
||||
if let current = self.content[content.id] {
|
||||
item = current
|
||||
} else {
|
||||
item = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
if let content = self.content {
|
||||
for content in content {
|
||||
if contentOriginY.isZero {
|
||||
contentOriginY += contentTopInset
|
||||
} else {
|
||||
contentOriginY += contentSpacing
|
||||
}
|
||||
self.content[content.id] = item
|
||||
}
|
||||
|
||||
let itemSize = item.update(
|
||||
transition: itemTransition,
|
||||
component: content.component,
|
||||
environment: { alertEnvironment },
|
||||
containerSize: CGSize(width: alertWidth - contentSideInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: contentSideInset, y: contentOriginY), size: itemSize)
|
||||
if let itemView = item.view {
|
||||
if itemView.superview == nil {
|
||||
self.backgroundView.contentView.addSubview(itemView)
|
||||
validContentIds.insert(content.id)
|
||||
|
||||
let item: ComponentView<AlertComponentEnvironment>
|
||||
var itemTransition = transition
|
||||
if let current = self.contentItems[content.id] {
|
||||
item = current
|
||||
} else {
|
||||
item = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.contentItems[content.id] = item
|
||||
}
|
||||
transition.setFrame(view: itemView, frame: itemFrame)
|
||||
|
||||
let itemSize = item.update(
|
||||
transition: itemTransition,
|
||||
component: content.component,
|
||||
environment: { alertEnvironment },
|
||||
containerSize: CGSize(width: alertWidth - contentSideInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: contentSideInset, y: contentOriginY), size: itemSize)
|
||||
if let itemView = item.view {
|
||||
if itemView.superview == nil {
|
||||
self.backgroundView.contentView.addSubview(itemView)
|
||||
item.parentState = state
|
||||
}
|
||||
transition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
contentOriginY += itemSize.height
|
||||
}
|
||||
contentOriginY += itemSize.height
|
||||
}
|
||||
|
||||
if !contentOriginY.isZero {
|
||||
|
|
@ -359,142 +393,150 @@ private final class AlertScreenComponent: Component {
|
|||
alertHeight += contentBottomInset
|
||||
}
|
||||
|
||||
let genericActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1),
|
||||
foreground: environment.theme.actionSheet.primaryTextColor,
|
||||
secondary: environment.theme.actionSheet.secondaryTextColor,
|
||||
font: .regular
|
||||
)
|
||||
let defaultActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.actionSheet.controlAccentColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
secondary: environment.theme.actionSheet.secondaryTextColor,
|
||||
font: .bold
|
||||
)
|
||||
let destructiveActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.list.itemDestructiveColor,
|
||||
foreground: .white,
|
||||
secondary: .white.withMultipliedAlpha(0.6),
|
||||
font: .regular
|
||||
)
|
||||
let defaultDestructiveActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.list.itemDestructiveColor,
|
||||
foreground: .white,
|
||||
secondary: .white.withMultipliedAlpha(0.6),
|
||||
font: .bold
|
||||
)
|
||||
|
||||
var effectiveActionLayout: ActionLayout = .horizontal
|
||||
if case .vertical = component.configuration.actionAlignment {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
var validActionIds: Set<AnyHashable> = Set()
|
||||
for action in component.actions {
|
||||
validActionIds.insert(action.id)
|
||||
|
||||
let item: ComponentView<AlertComponentEnvironment>
|
||||
var itemTransition = transition
|
||||
if let current = self.actions[action.id] {
|
||||
item = current
|
||||
} else {
|
||||
item = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.actions[action.id] = item
|
||||
}
|
||||
|
||||
let actionTheme: AlertActionComponent.Theme
|
||||
switch action.type {
|
||||
case .generic:
|
||||
actionTheme = genericActionTheme
|
||||
case .default:
|
||||
actionTheme = defaultActionTheme
|
||||
case .destructive:
|
||||
actionTheme = destructiveActionTheme
|
||||
case .defaultDestructive:
|
||||
actionTheme = defaultDestructiveActionTheme
|
||||
}
|
||||
let itemSize = item.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(AlertActionComponent(
|
||||
theme: actionTheme,
|
||||
title: action.title,
|
||||
isHighlighted: AnyHashable(action.id) == self.highlightedAction,
|
||||
progress: action.progressPromise.get()
|
||||
)),
|
||||
environment: { alertEnvironment },
|
||||
containerSize: fullWidthActionSize
|
||||
if let actions = self.actions {
|
||||
let genericActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1),
|
||||
foreground: environment.theme.actionSheet.primaryTextColor,
|
||||
secondary: environment.theme.actionSheet.secondaryTextColor,
|
||||
font: .regular
|
||||
)
|
||||
if let itemView = item.view {
|
||||
if itemView.superview == nil {
|
||||
self.backgroundView.contentView.addSubview(itemView)
|
||||
let defaultActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.actionSheet.controlAccentColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
secondary: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85),
|
||||
font: .bold
|
||||
)
|
||||
let destructiveActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.list.itemDestructiveColor,
|
||||
foreground: .white,
|
||||
secondary: .white.withMultipliedAlpha(0.6),
|
||||
font: .regular
|
||||
)
|
||||
let defaultDestructiveActionTheme = AlertActionComponent.Theme(
|
||||
background: environment.theme.list.itemDestructiveColor,
|
||||
foreground: .white,
|
||||
secondary: .white.withMultipliedAlpha(0.6),
|
||||
font: .bold
|
||||
)
|
||||
|
||||
var effectiveActionLayout: ActionLayout = .horizontal
|
||||
if case .vertical = component.configuration.actionAlignment {
|
||||
effectiveActionLayout = .vertical
|
||||
} else if actions.count == 1 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
var actionTransitions: [AnyHashable: ComponentTransition] = [:]
|
||||
var validActionIds: Set<AnyHashable> = Set()
|
||||
for action in actions {
|
||||
validActionIds.insert(action.id)
|
||||
|
||||
let item: ComponentView<AlertComponentEnvironment>
|
||||
var itemTransition = transition
|
||||
if let current = self.actionItems[action.id] {
|
||||
item = current
|
||||
} else {
|
||||
item = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.actionItems[action.id] = item
|
||||
}
|
||||
actionTransitions[action.id] = itemTransition
|
||||
|
||||
let actionTheme: AlertActionComponent.Theme
|
||||
switch action.type {
|
||||
case .generic:
|
||||
actionTheme = genericActionTheme
|
||||
case .default:
|
||||
actionTheme = defaultActionTheme
|
||||
case .destructive:
|
||||
actionTheme = destructiveActionTheme
|
||||
case .defaultDestructive:
|
||||
actionTheme = defaultDestructiveActionTheme
|
||||
}
|
||||
let itemSize = item.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(AlertActionComponent(
|
||||
theme: actionTheme,
|
||||
title: action.title,
|
||||
isHighlighted: AnyHashable(action.id) == self.highlightedAction,
|
||||
isEnabled: action.isEnabled,
|
||||
progress: action.progress
|
||||
)),
|
||||
environment: { alertEnvironment },
|
||||
containerSize: fullWidthActionSize
|
||||
)
|
||||
if let itemView = item.view {
|
||||
if itemView.superview == nil {
|
||||
self.backgroundView.contentView.addSubview(itemView)
|
||||
}
|
||||
}
|
||||
|
||||
if case .horizontal = effectiveActionLayout, itemSize.width > halfWidthActionSize.width {
|
||||
effectiveActionLayout = .verticalReversed
|
||||
}
|
||||
}
|
||||
self.effectiveActionLayout = effectiveActionLayout
|
||||
|
||||
if case .horizontal = effectiveActionLayout, itemSize.width > halfWidthActionSize.width {
|
||||
effectiveActionLayout = .verticalReversed
|
||||
if !actions.isEmpty {
|
||||
let actionsHeight: CGFloat
|
||||
if self.effectiveActionLayout.isVertical {
|
||||
actionsHeight = fullWidthActionSize.height * CGFloat(actions.count) + actionSpacing * CGFloat(actions.count - 1)
|
||||
} else {
|
||||
actionsHeight = fullWidthActionSize.height
|
||||
}
|
||||
alertHeight += actionsHeight
|
||||
alertHeight += actionSideInset
|
||||
}
|
||||
}
|
||||
self.effectiveActionLayout = effectiveActionLayout
|
||||
|
||||
if !component.actions.isEmpty {
|
||||
let actionsHeight: CGFloat
|
||||
if self.effectiveActionLayout.isVertical {
|
||||
actionsHeight = fullWidthActionSize.height * CGFloat(component.actions.count) + actionSpacing * CGFloat(component.actions.count - 1)
|
||||
} else {
|
||||
actionsHeight = fullWidthActionSize.height
|
||||
}
|
||||
alertHeight += actionsHeight
|
||||
alertHeight += actionSideInset
|
||||
}
|
||||
|
||||
var actionOriginX: CGFloat = actionSideInset
|
||||
var actionOriginY: CGFloat
|
||||
switch self.effectiveActionLayout {
|
||||
case .horizontal, .verticalReversed:
|
||||
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height
|
||||
case .vertical:
|
||||
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height * CGFloat(component.actions.count) - actionSpacing * CGFloat(component.actions.count - 1)
|
||||
}
|
||||
for action in component.actions {
|
||||
guard let item = self.actions[action.id], let itemView = item.view as? AlertActionComponent.View else {
|
||||
continue
|
||||
}
|
||||
let itemFrame: CGRect
|
||||
|
||||
var actionOriginX: CGFloat = actionSideInset
|
||||
var actionOriginY: CGFloat
|
||||
switch self.effectiveActionLayout {
|
||||
case .horizontal:
|
||||
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: halfWidthActionSize)
|
||||
actionOriginX += halfWidthActionSize.width + actionSpacing
|
||||
case .horizontal, .verticalReversed:
|
||||
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height
|
||||
case .vertical:
|
||||
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
|
||||
actionOriginY += fullWidthActionSize.height + actionSpacing
|
||||
case .verticalReversed:
|
||||
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
|
||||
actionOriginY -= fullWidthActionSize.height + actionSpacing
|
||||
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height * CGFloat( actions.count) - actionSpacing * CGFloat(actions.count - 1)
|
||||
}
|
||||
itemView.applySize(size: itemFrame.size, transition: transition)
|
||||
transition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
|
||||
var removeActionIds: [AnyHashable] = []
|
||||
for (id, item) in self.actions {
|
||||
if !validActionIds.contains(id) {
|
||||
removeActionIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
for action in actions {
|
||||
guard let item = self.actionItems[action.id], let itemView = item.view as? AlertActionComponent.View else {
|
||||
continue
|
||||
}
|
||||
let itemTransition = actionTransitions[action.id] ?? transition
|
||||
let itemFrame: CGRect
|
||||
switch self.effectiveActionLayout {
|
||||
case .horizontal:
|
||||
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: halfWidthActionSize)
|
||||
actionOriginX += halfWidthActionSize.width + actionSpacing
|
||||
case .vertical:
|
||||
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
|
||||
actionOriginY += fullWidthActionSize.height + actionSpacing
|
||||
case .verticalReversed:
|
||||
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
|
||||
actionOriginY -= fullWidthActionSize.height + actionSpacing
|
||||
}
|
||||
itemView.applySize(size: itemFrame.size, transition: itemTransition)
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
|
||||
var removeActionIds: [AnyHashable] = []
|
||||
for (id, item) in self.actionItems {
|
||||
if !validActionIds.contains(id) {
|
||||
removeActionIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeActionIds {
|
||||
self.actions.removeValue(forKey: id)
|
||||
for id in removeActionIds {
|
||||
self.actionItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
let alertSize = CGSize(width: alertWidth, height: alertHeight)
|
||||
|
|
@ -504,7 +546,12 @@ private final class AlertScreenComponent: Component {
|
|||
transition.setFrame(view: self.containerView, frame: bounds)
|
||||
self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - alertSize.height) / 2.0)), size: alertSize))
|
||||
var availableHeight = availableSize.height
|
||||
if component.configuration.allowInputInset, environment.inputHeight > 0.0 {
|
||||
availableHeight -= environment.inputHeight
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize))
|
||||
self.backgroundView.update(size: alertSize, shape: .roundedRect(cornerRadius: 35.0), isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: .white), isInteractive: true, transition: transition)
|
||||
|
||||
return availableSize
|
||||
|
|
@ -520,7 +567,7 @@ private final class AlertScreenComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder {
|
||||
open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder {
|
||||
public enum ActionAligmnent: Equatable {
|
||||
case `default`
|
||||
case vertical
|
||||
|
|
@ -532,22 +579,14 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
let allowInputInset: Bool
|
||||
|
||||
public init(
|
||||
actionAlignment: ActionAligmnent,
|
||||
dismissOnOutsideTap: Bool,
|
||||
allowInputInset: Bool
|
||||
actionAlignment: ActionAligmnent = .default,
|
||||
dismissOnOutsideTap: Bool = true,
|
||||
allowInputInset: Bool = false
|
||||
) {
|
||||
self.actionAlignment = actionAlignment
|
||||
self.dismissOnOutsideTap = dismissOnOutsideTap
|
||||
self.allowInputInset = allowInputInset
|
||||
}
|
||||
|
||||
public static var defaultValue: Configuration {
|
||||
return Configuration(
|
||||
actionAlignment: .default,
|
||||
dismissOnOutsideTap: true,
|
||||
allowInputInset: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Action: Equatable {
|
||||
|
|
@ -561,20 +600,30 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
public let type: ActionType
|
||||
public let action: () -> Void
|
||||
public let autoDismiss: Bool
|
||||
public let progressPromise: ValuePromise<Bool>
|
||||
public let isEnabled: Signal<Bool, NoError>
|
||||
public let progress: Signal<Bool, NoError>
|
||||
|
||||
public init(
|
||||
id: AnyHashable? = nil,
|
||||
title: String,
|
||||
type: ActionType = .generic,
|
||||
action: @escaping () -> Void = {},
|
||||
autoDismiss: Bool = true,
|
||||
progressPromise: ValuePromise<Bool> = ValuePromise(false)
|
||||
isEnabled: Signal<Bool, NoError> = .single(true),
|
||||
progress: Signal<Bool, NoError> = .single(false)
|
||||
) {
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.action = action
|
||||
self.autoDismiss = autoDismiss
|
||||
self.progressPromise = progressPromise
|
||||
self.isEnabled = isEnabled
|
||||
self.progress = progress
|
||||
|
||||
if let id {
|
||||
self.id = id
|
||||
} else {
|
||||
self.id = title
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: Action, rhs: Action) -> Bool {
|
||||
|
|
@ -590,7 +639,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
return true
|
||||
}
|
||||
|
||||
fileprivate let id: Int64 = Int64.random(in: Int64.min ..< Int64.max)
|
||||
fileprivate let id: AnyHashable
|
||||
}
|
||||
|
||||
private var processedDidAppear: Bool = false
|
||||
|
|
@ -604,9 +653,9 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
public var dismissed: ((Bool) -> Void)?
|
||||
|
||||
public init(
|
||||
configuration: Configuration = .defaultValue,
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [Action],
|
||||
configuration: Configuration = Configuration(),
|
||||
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
|
||||
actionsSignal: Signal<[Action], NoError>,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
|
||||
) {
|
||||
let componentReady = Promise<Bool>()
|
||||
|
|
@ -614,8 +663,8 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
super.init(
|
||||
component: AlertScreenComponent(
|
||||
configuration: configuration,
|
||||
content: content,
|
||||
actions: actions,
|
||||
content: contentSignal,
|
||||
actions: actionsSignal,
|
||||
ready: componentReady
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
|
|
@ -628,14 +677,42 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
//self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
configuration: Configuration = Configuration(),
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [Action],
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
|
||||
) {
|
||||
self.init(
|
||||
configuration: configuration,
|
||||
contentSignal: .single(content),
|
||||
actionsSignal: .single(actions),
|
||||
updatedPresentationData: updatedPresentationData
|
||||
)
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
context: AccountContext,
|
||||
configuration: Configuration = .defaultValue,
|
||||
configuration: Configuration = Configuration(),
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [Action]
|
||||
) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let updatedPresentationDataSignal = context.sharedContext.presentationData
|
||||
self.init(
|
||||
sharedContext: context.sharedContext,
|
||||
configuration: configuration,
|
||||
content: content,
|
||||
actions: actions,
|
||||
)
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
sharedContext: SharedAccountContext,
|
||||
configuration: Configuration = Configuration(),
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [Action]
|
||||
) {
|
||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
let updatedPresentationDataSignal = sharedContext.presentationData
|
||||
self.init(
|
||||
configuration: configuration,
|
||||
content: content,
|
||||
|
|
@ -645,7 +722,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
}
|
||||
|
||||
public convenience init(
|
||||
configuration: Configuration = .defaultValue,
|
||||
configuration: Configuration = Configuration(),
|
||||
title: String? = nil,
|
||||
text: String,
|
||||
actions: [Action],
|
||||
|
|
@ -660,12 +737,14 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
)
|
||||
))
|
||||
}
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
if !text.isEmpty {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
self.init(
|
||||
configuration: configuration,
|
||||
|
|
@ -677,7 +756,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
|
||||
public convenience init(
|
||||
context: AccountContext,
|
||||
configuration: Configuration = .defaultValue,
|
||||
configuration: Configuration = Configuration(),
|
||||
title: String? = nil,
|
||||
text: String,
|
||||
actions: [Action]
|
||||
|
|
@ -700,10 +779,6 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
deinit {
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
|
|
@ -719,7 +794,7 @@ public class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder
|
|||
super.dismiss()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
override open func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.processedDidDisappear {
|
||||
self.processedDidDisappear = true
|
||||
|
||||
|
|
|
|||
|
|
@ -3,32 +3,53 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
|
||||
private let titleFont = Font.bold(17.0)
|
||||
private let defaultTextFont = Font.regular(15.0)
|
||||
private let defaultBoldTextFont = Font.semibold(15.0)
|
||||
private let defaultItalicTextFont = Font.italic(15.0)
|
||||
private let defaultBoldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: [.italic])
|
||||
private let defaultFixedTextFont = Font.monospace(15.0)
|
||||
private let smallTextFont = Font.regular(14.0)
|
||||
private let smallBoldTextFont = Font.semibold(14.0)
|
||||
private let smallItalicTextFont = Font.italic(14.0)
|
||||
private let smallBoldItalicTextFont = Font.with(size: 14.0, weight: .semibold, traits: [.italic])
|
||||
private let smallFixedTextFont = Font.monospace(14.0)
|
||||
private let backgroundInset: CGFloat = 8.0
|
||||
|
||||
public final class AlertTitleComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public enum Alignment {
|
||||
case `default`
|
||||
case center
|
||||
}
|
||||
|
||||
let title: String
|
||||
let alignment: Alignment
|
||||
|
||||
public init(
|
||||
title: String
|
||||
title: String,
|
||||
alignment: Alignment = .default
|
||||
) {
|
||||
self.title = title
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTitleComponent, rhs: AlertTitleComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.alignment != rhs.alignment {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -52,12 +73,21 @@ public final class AlertTitleComponent: Component {
|
|||
font: titleFont,
|
||||
textColor: environment.theme.actionSheet.primaryTextColor
|
||||
)),
|
||||
horizontalAlignment: component.alignment == .center ? .center : .natural,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let titleFrame = CGRect(origin: .zero, size: titleSize)
|
||||
|
||||
let titleOriginX: CGFloat
|
||||
switch component.alignment {
|
||||
case .default:
|
||||
titleOriginX = 0.0
|
||||
case .center:
|
||||
titleOriginX = floorToScreenPixels((availableSize.width - titleSize.width) / 2.0)
|
||||
}
|
||||
let titleFrame = CGRect(origin: CGPoint(x: titleOriginX, y: 0.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
|
|
@ -83,33 +113,74 @@ public final class AlertTextComponent: Component {
|
|||
public enum Content: Equatable {
|
||||
case plain(String)
|
||||
case attributed(NSAttributedString)
|
||||
case textWithEntities(AccountContext, String, [MessageTextEntity])
|
||||
|
||||
public static func ==(lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
case let .plain(text):
|
||||
if case .plain(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .attributed(text):
|
||||
if case .attributed(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .textWithEntities(_, lhsText, lhsEntities):
|
||||
if case let .textWithEntities(_, rhsText, rhsEntities) = rhs {
|
||||
return lhsText == rhsText && lhsEntities == rhsEntities
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Color {
|
||||
public enum Alignment: Equatable {
|
||||
case `default`
|
||||
case center
|
||||
}
|
||||
|
||||
public enum Color: Equatable {
|
||||
case primary
|
||||
case secondary
|
||||
case destructive
|
||||
}
|
||||
|
||||
public enum TextSize {
|
||||
public enum TextStyle: Equatable {
|
||||
case `default`
|
||||
case small
|
||||
case bold
|
||||
}
|
||||
|
||||
public enum Style: Equatable {
|
||||
case plain(TextStyle)
|
||||
case background(TextStyle)
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let alignment: Alignment
|
||||
let color: Color
|
||||
let textSize: TextSize
|
||||
let style: Style
|
||||
let insets: UIEdgeInsets
|
||||
let action: ([NSAttributedString.Key: Any]) -> Void
|
||||
|
||||
public init(
|
||||
content: Content,
|
||||
alignment: Alignment = .default,
|
||||
color: Color = .primary,
|
||||
textSize: TextSize = .default,
|
||||
style: Style = .plain(.default),
|
||||
insets: UIEdgeInsets = .zero,
|
||||
action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in }
|
||||
) {
|
||||
self.content = content
|
||||
self.alignment = alignment
|
||||
self.color = color
|
||||
self.textSize = textSize
|
||||
self.style = style
|
||||
self.insets = insets
|
||||
self.action = action
|
||||
}
|
||||
|
||||
|
|
@ -117,16 +188,23 @@ public final class AlertTextComponent: Component {
|
|||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.textSize != rhs.textSize {
|
||||
if lhs.alignment != rhs.alignment {
|
||||
return false
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let background = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTextComponent?
|
||||
|
|
@ -143,7 +221,7 @@ public final class AlertTextComponent: Component {
|
|||
case .primary:
|
||||
textColor = environment.theme.actionSheet.primaryTextColor
|
||||
case .secondary:
|
||||
textColor = environment.theme.actionSheet.secondaryTextColor
|
||||
textColor = environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35)
|
||||
case .destructive:
|
||||
textColor = environment.theme.actionSheet.destructiveActionTextColor
|
||||
}
|
||||
|
|
@ -151,16 +229,31 @@ public final class AlertTextComponent: Component {
|
|||
|
||||
let textFont: UIFont
|
||||
let boldTextFont: UIFont
|
||||
switch component.textSize {
|
||||
case .default:
|
||||
textFont = defaultTextFont
|
||||
boldTextFont = defaultBoldTextFont
|
||||
case .small:
|
||||
textFont = smallTextFont
|
||||
boldTextFont = smallBoldTextFont
|
||||
let italicTextFont: UIFont
|
||||
let fixedTextFont: UIFont
|
||||
switch component.style {
|
||||
case let .plain(textStyle), let .background(textStyle):
|
||||
switch textStyle {
|
||||
case .default:
|
||||
textFont = defaultTextFont
|
||||
boldTextFont = defaultBoldTextFont
|
||||
italicTextFont = defaultItalicTextFont
|
||||
fixedTextFont = defaultFixedTextFont
|
||||
case .small:
|
||||
textFont = smallTextFont
|
||||
boldTextFont = smallBoldTextFont
|
||||
italicTextFont = smallItalicTextFont
|
||||
fixedTextFont = smallFixedTextFont
|
||||
case .bold:
|
||||
textFont = defaultBoldTextFont
|
||||
boldTextFont = defaultBoldTextFont
|
||||
italicTextFont = defaultBoldItalicTextFont
|
||||
fixedTextFont = defaultFixedTextFont
|
||||
}
|
||||
}
|
||||
|
||||
var finalText: NSAttributedString
|
||||
var context: AccountContext?
|
||||
switch component.content {
|
||||
case let .plain(text):
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
|
|
@ -174,37 +267,92 @@ public final class AlertTextComponent: Component {
|
|||
finalText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes)
|
||||
case let .attributed(attributedText):
|
||||
finalText = attributedText
|
||||
case let .textWithEntities(accountContext, text, entities):
|
||||
context = accountContext
|
||||
finalText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: italicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil)
|
||||
}
|
||||
|
||||
var hasCenterAlignment = component.alignment == .center
|
||||
switch component.style {
|
||||
case .background:
|
||||
hasCenterAlignment = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let textConstrainedSize = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(finalText),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1,
|
||||
highlightColor: linkColor.withAlphaComponent(0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
component: AnyComponent(
|
||||
MultilineTextWithEntitiesComponent(
|
||||
context: context,
|
||||
animationCache: context?.animationCache,
|
||||
animationRenderer: context?.animationRenderer,
|
||||
placeholderColor: textColor.withMultipliedAlpha(0.1),
|
||||
text: .plain(finalText),
|
||||
horizontalAlignment: hasCenterAlignment ? .center : .natural,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
spoilerColor: textColor,
|
||||
highlightColor: linkColor.withAlphaComponent(0.2),
|
||||
manualVisibilityControl: true,
|
||||
resetAnimationsOnVisibilityChange: true,
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
component.action(attributes)
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
component.action(attributes)
|
||||
}
|
||||
)),
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: textConstrainedSize
|
||||
)
|
||||
let textFrame = CGRect(origin: .zero, size: textSize)
|
||||
|
||||
var textOffset = CGPoint()
|
||||
if hasCenterAlignment {
|
||||
textOffset.x = floorToScreenPixels((availableSize.width - textSize.width) / 2.0)
|
||||
}
|
||||
var size = CGSize(width: availableSize.width, height: textSize.height)
|
||||
if case .background = component.style {
|
||||
let backgroundSize = CGSize(width: availableSize.width + 20.0, height: textSize.height + backgroundInset * 2.0)
|
||||
size = backgroundSize
|
||||
textOffset = CGPoint(x: textOffset.x, y: backgroundInset)
|
||||
|
||||
let _ = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
FilledRoundedRectangleComponent(
|
||||
color: textColor.withMultipliedAlpha(0.1),
|
||||
cornerRadius: .value(10.0),
|
||||
smoothCorners: true
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: backgroundSize
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: component.insets.top), size: backgroundSize)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
}
|
||||
|
||||
let textFrame = CGRect(origin: textOffset.offsetBy(dx: 0.0, dy: component.insets.top), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
transition.setFrame(view: textView, frame: textFrame)
|
||||
}
|
||||
return CGSize(width: availableSize.width, height: textSize.height)
|
||||
return CGSize(width: size.width, height: size.height + component.insets.top + component.insets.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,395 +0,0 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import AsyncDisplayKit
|
||||
|
||||
private let alertWidth: CGFloat = 270.0
|
||||
|
||||
public enum ComponentAlertActionType {
|
||||
case genericAction
|
||||
case defaultAction
|
||||
case destructiveAction
|
||||
case defaultDestructiveAction
|
||||
}
|
||||
|
||||
public struct ComponentAlertAction {
|
||||
public let type: ComponentAlertActionType
|
||||
public let title: String
|
||||
public let action: () -> Void
|
||||
|
||||
public init(type: ComponentAlertActionType, title: String, action: @escaping () -> Void) {
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
public final class ComponentAlertContentActionNode: HighlightableButtonNode {
|
||||
private var theme: AlertControllerTheme
|
||||
public var action: ComponentAlertAction {
|
||||
didSet {
|
||||
self.updateTitle()
|
||||
}
|
||||
}
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
|
||||
public var highlightedUpdated: (Bool) -> Void = { _ in }
|
||||
|
||||
public init(theme: AlertControllerTheme, action: ComponentAlertAction) {
|
||||
self.theme = theme
|
||||
self.action = action
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.highligthedChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.setHighlighted(value, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.setHighlighted(true, animated: false)
|
||||
}
|
||||
}, willExit: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.setHighlighted(false, animated: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func performAction() {
|
||||
if self.actionEnabled {
|
||||
self.action.action()
|
||||
}
|
||||
}
|
||||
|
||||
public func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
self.highlightedUpdated(highlighted)
|
||||
if highlighted {
|
||||
if self.backgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.backgroundNode, at: 0)
|
||||
}
|
||||
self.backgroundNode.alpha = 1.0
|
||||
} else {
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
self.backgroundNode.alpha = 0.0
|
||||
})
|
||||
} else {
|
||||
self.backgroundNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
public var actionEnabled: Bool = true {
|
||||
didSet {
|
||||
self.isUserInteractionEnabled = self.actionEnabled
|
||||
self.updateTitle()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.theme = theme
|
||||
self.backgroundNode.backgroundColor = theme.highlightedItemColor
|
||||
self.updateTitle()
|
||||
}
|
||||
|
||||
private func updateTitle() {
|
||||
var font = Font.regular(theme.baseFontSize)
|
||||
var color: UIColor
|
||||
switch self.action.type {
|
||||
case .defaultAction, .genericAction:
|
||||
color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor
|
||||
case .destructiveAction, .defaultDestructiveAction:
|
||||
color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor
|
||||
}
|
||||
switch self.action.type {
|
||||
case .defaultAction, .defaultDestructiveAction:
|
||||
font = Font.semibold(theme.baseFontSize)
|
||||
case .destructiveAction, .genericAction:
|
||||
break
|
||||
}
|
||||
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
|
||||
self.accessibilityLabel = self.action.title
|
||||
self.accessibilityTraits = [.button]
|
||||
}
|
||||
|
||||
@objc func pressed() {
|
||||
self.action.action()
|
||||
}
|
||||
|
||||
override public func layout() {
|
||||
super.layout()
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
public enum ComponentAlertContentActionLayout {
|
||||
case horizontal
|
||||
case vertical
|
||||
}
|
||||
|
||||
public final class ComponentAlertContentNode: AlertContentNode {
|
||||
private var theme: AlertControllerTheme
|
||||
private let actionLayout: ComponentAlertContentActionLayout
|
||||
|
||||
private let content: AnyComponent<Empty>
|
||||
private let contentView = ComponentView<Empty>()
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [ComponentAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let _dismissOnOutsideTap: Bool
|
||||
override public var dismissOnOutsideTap: Bool {
|
||||
return self._dismissOnOutsideTap
|
||||
}
|
||||
|
||||
private var highlightedItemIndex: Int? = nil
|
||||
|
||||
public init(theme: AlertControllerTheme, content: AnyComponent<Empty>, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout, dismissOnOutsideTap: Bool) {
|
||||
self.theme = theme
|
||||
self.actionLayout = actionLayout
|
||||
self._dismissOnOutsideTap = dismissOnOutsideTap
|
||||
self.content = content
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isUserInteractionEnabled = false
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
|
||||
self.actionNodes = actions.map { action -> ComponentAlertContentActionNode in
|
||||
return ComponentAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
var i = 0
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
|
||||
let index = i
|
||||
actionNode.highlightedUpdated = { [weak self] highlighted in
|
||||
if highlighted {
|
||||
self?.highlightedItemIndex = index
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
}
|
||||
|
||||
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
|
||||
self.highlightedItemIndex = index
|
||||
|
||||
if update {
|
||||
var i = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if i == index {
|
||||
actionNode.setHighlighted(true, animated: false)
|
||||
} else {
|
||||
actionNode.setHighlighted(false, animated: false)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func decreaseHighlightedIndex() {
|
||||
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
|
||||
|
||||
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
|
||||
}
|
||||
|
||||
override public func increaseHighlightedIndex() {
|
||||
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
|
||||
|
||||
self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true)
|
||||
}
|
||||
|
||||
override public func performHighlightedAction() {
|
||||
guard let highlightedItemIndex = self.highlightedItemIndex else {
|
||||
return
|
||||
}
|
||||
|
||||
var i = 0
|
||||
for itemNode in self.actionNodes {
|
||||
if i == highlightedItemIndex {
|
||||
itemNode.performAction()
|
||||
return
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
self.validLayout = size
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var size = size
|
||||
size.width = min(size.width, alertWidth)
|
||||
|
||||
let contentSize = self.contentView.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: self.content,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - insets.left - insets.right, height: 10000.0)
|
||||
)
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = self.actionLayout
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let resultSize: CGSize
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let contentWidth = alertWidth - insets.left - insets.right
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - contentSize.width) / 2.0), y: insets.top), size: contentSize)
|
||||
if let contentComponentView = self.contentView.view {
|
||||
if contentComponentView.superview == nil {
|
||||
self.view.insertSubview(contentComponentView, belowSubview: self.actionNodesSeparator.view)
|
||||
transition.updateFrame(view: contentComponentView, frame: contentFrame)
|
||||
}
|
||||
}
|
||||
|
||||
resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: contentSize.height + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public func componentAlertController(theme: AlertControllerTheme, content: AnyComponent<Empty>, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true) -> AlertController {
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = AlertController(theme: theme, contentNode: ComponentAlertContentNode(theme: theme, content: content, actions: actions.map { action in
|
||||
return ComponentAlertAction(type: action.type, title: action.title, action: {
|
||||
dismissImpl?()
|
||||
action.action()
|
||||
})
|
||||
}, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap))
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
|
@ -212,7 +212,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
|
|||
|
||||
if case .audio = mode {
|
||||
if let savedMusic, savedMusic.count > 0 {
|
||||
entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased()))
|
||||
entries.append(.savedHeader(presentationData.theme, presentationData.strings.MediaEditor_Audio_SavedMusic.uppercased()))
|
||||
var savedMusic = savedMusic
|
||||
var showMore = false
|
||||
if savedMusic.count > 4 && !state.savedMusicExpanded {
|
||||
|
|
@ -225,7 +225,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
|
|||
i += 1
|
||||
}
|
||||
if showMore {
|
||||
entries.append(.showMore(presentationData.theme, "Show More"))
|
||||
entries.append(.showMore(presentationData.theme, presentationData.strings.MediaEditor_Audio_ShowMore))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -465,6 +465,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
|||
backgroundColor: nil,
|
||||
isDark: presentationData.theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
animateScale: false,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
|
|
@ -492,6 +493,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
|||
backgroundColor: nil,
|
||||
isDark: presentationData.theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
animateScale: false,
|
||||
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Search",
|
||||
|
|
@ -542,8 +544,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
|||
case .recent:
|
||||
title = presentationData.strings.Attachment_File
|
||||
case .audio:
|
||||
//TODO:localize
|
||||
title = "Audio"
|
||||
title = presentationData.strings.MediaEditor_Audio_Title
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
|
|
|
|||
24
submodules/TelegramUI/Components/AvatarComponent/BUILD
Normal file
24
submodules/TelegramUI/Components/AvatarComponent/BUILD
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AvatarComponent",
|
||||
module_name = "AvatarComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
|
||||
public final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer
|
||||
let icon: AnyComponent<Empty>?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer,
|
||||
icon: AnyComponent<Empty>? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
public static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
private var icon: ComponentView<Empty>?
|
||||
|
||||
private var component: AvatarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
if self.component == nil {
|
||||
self.avatarNode.font = avatarPlaceholderFont(size: ceil(42.0 * availableSize.width / 100.0))
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var cutoutRect: CGRect?
|
||||
if let icon = component.icon {
|
||||
let iconView: ComponentView<Empty>
|
||||
if let current = self.icon {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = ComponentView()
|
||||
self.icon = iconView
|
||||
}
|
||||
let iconSize = iconView.update(
|
||||
transition: .immediate,
|
||||
component: icon,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize)
|
||||
if let iconView = iconView.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel)
|
||||
}
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: component.peer,
|
||||
synchronousLoad: true,
|
||||
cutoutRect: cutoutRect
|
||||
)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/AvatarBackground",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -10,263 +10,117 @@ import TelegramPresentationData
|
|||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import AlertComponent
|
||||
import PremiumStarComponent
|
||||
|
||||
private final class PremiumAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let presentationTheme: PresentationTheme
|
||||
private let title: String?
|
||||
private let text: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
private let icon = ComponentView<Empty>()
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(
|
||||
theme: AlertControllerTheme,
|
||||
presentationTheme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
title: String?,
|
||||
text: String,
|
||||
actions: [TextAlertAction]
|
||||
) {
|
||||
self.strings = strings
|
||||
self.presentationTheme = presentationTheme
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.lineSpacing = 0.1
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? self.strings.PremiumNeeded_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width , 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let starHeight: CGFloat = 105.0
|
||||
let starSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PremiumStarComponent(
|
||||
theme: self.presentationTheme,
|
||||
isIntro: false,
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
colors: [
|
||||
UIColor(rgb: 0x6a94ff),
|
||||
UIColor(rgb: 0x9472fd),
|
||||
UIColor(rgb: 0xe26bd3)
|
||||
]
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 200.0)
|
||||
)
|
||||
if let view = self.icon.view {
|
||||
if view.superview == nil {
|
||||
self.view.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 0.0, y: -36.0), size: starSize)
|
||||
origin.y += starHeight
|
||||
}
|
||||
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 5.0
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + starHeight + textSize.height + actionsHeight + 7.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
func premiumAlertController(context: AccountContext, parentController: ViewController, title: String? = nil, text: String) -> AlertController {
|
||||
func premiumAlertController(
|
||||
context: AccountContext,
|
||||
parentController: ViewController,
|
||||
title: String? = nil,
|
||||
text: String
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var proceedImpl: (() -> Void)?
|
||||
var contentNode: PremiumAlertContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.PremiumNeeded_Subscribe, action: {
|
||||
proceedImpl?()
|
||||
|
||||
dismissImpl?(true)
|
||||
})]
|
||||
|
||||
contentNode = PremiumAlertContentNode(
|
||||
theme: AlertControllerTheme(presentationData: presentationData),
|
||||
presentationTheme: presentationData.theme,
|
||||
strings: strings,
|
||||
title: title,
|
||||
text: text,
|
||||
actions: actions
|
||||
)
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "header",
|
||||
component: AnyComponent(
|
||||
AlertPremiumStarComponent()
|
||||
)
|
||||
))
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
let title = title ?? strings.PremiumNeeded_Title
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
|
||||
let alertController = AlertScreen(
|
||||
context: context,
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.PremiumNeeded_Subscribe, type: .default, action: { [weak parentController] in
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil)
|
||||
parentController?.push(controller)
|
||||
})
|
||||
]
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
||||
private final class AlertPremiumStarComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertPremiumStarComponent, rhs: AlertPremiumStarComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let clippingView = UIView()
|
||||
private let icon = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertPremiumStarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertPremiumStarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let starHeight: CGFloat = 105.0
|
||||
let starSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PremiumStarComponent(
|
||||
theme: environment.theme,
|
||||
isIntro: false,
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
colors: [
|
||||
UIColor(rgb: 0x6a94ff),
|
||||
UIColor(rgb: 0x9472fd),
|
||||
UIColor(rgb: 0xe26bd3)
|
||||
]
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + 60.0, height: 200.0)
|
||||
)
|
||||
if let view = self.icon.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(self.clippingView)
|
||||
self.clippingView.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 0.0, y: -24.0), size: starSize)
|
||||
}
|
||||
|
||||
self.clippingView.clipsToBounds = true
|
||||
self.clippingView.layer.cornerRadius = 35.0
|
||||
self.clippingView.frame = CGRect(origin: CGPoint(x: -30.0, y: -22.0), size: CGSize(width: starSize.width, height: starSize.height))
|
||||
|
||||
return CGSize(width: availableSize.width, height: starHeight + 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
proceedImpl = { [weak parentController] in
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil)
|
||||
parentController?.push(controller)
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
return controller
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatAgeRestrictionAlertController",
|
||||
module_name = "ChatAgeRestrictionAlertController",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertCheckComponent
|
||||
|
||||
public func chatAgeRestrictionAlertController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
parentController: ViewController,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
let checkState = AlertCheckComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.SensitiveContent_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.SensitiveContent_Text))
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "check",
|
||||
component: AnyComponent(
|
||||
AlertCheckComponent(title: strings.SensitiveContent_ShowAlways, initialValue: false, externalState: checkState)
|
||||
)
|
||||
))
|
||||
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.SensitiveContent_ViewAnyway, type: .default, action: {
|
||||
completion(checkState.value)
|
||||
}),
|
||||
.init(title: strings.Common_Cancel)
|
||||
],
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
|
@ -488,6 +488,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
Queue.mainQueue().after(3.0, {
|
||||
self.labelNode.textNode.alpha = 1.0
|
||||
self.labelBackgroundNode?.alpha = 1.0
|
||||
self.labelNode.textNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
self.labelBackgroundNode?.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
self.labelNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.labelBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
})
|
||||
|
|
@ -1127,6 +1129,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
var viewCount: Int? = nil
|
||||
var dateReplies = 0
|
||||
var starsCount: Int64?
|
||||
var tonAmount: Int64?
|
||||
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
|
||||
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
|
||||
dateReactionsAndPeers = ([], [])
|
||||
|
|
@ -1145,6 +1148,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
}
|
||||
}
|
||||
|
||||
if let stakeTonAmount = telegramDice?.tonAmount {
|
||||
tonAmount = stakeTonAmount
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, associatedData: item.associatedData)
|
||||
|
||||
var isReplyThread = false
|
||||
|
|
@ -1173,6 +1180,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
messageEffect: messageEffect,
|
||||
replyCount: dateReplies,
|
||||
starsCount: starsCount,
|
||||
tonAmount: tonAmount,
|
||||
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
||||
hasAutoremove: item.message.isSelfExpiring,
|
||||
canViewReactionList: canViewMessageReactionList(message: item.message),
|
||||
|
|
@ -1277,6 +1285,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
|
@ -1559,6 +1568,30 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
}
|
||||
}
|
||||
|
||||
var updatedImageFrame: CGRect
|
||||
var contextContentFrame: CGRect
|
||||
if let _ = emojiString {
|
||||
updatedImageFrame = imageFrame
|
||||
contextContentFrame = updatedImageFrame.inset(by: UIEdgeInsets(top: 0.0, left: 0.0, bottom: -imageBottomPadding, right: 0.0))
|
||||
} else {
|
||||
updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0))
|
||||
contextContentFrame = updatedImageFrame
|
||||
|
||||
if let telegramDice, let _ = telegramDice.tonAmount {
|
||||
updatedImageFrame = updatedImageFrame.offsetBy(dx: 0.0, dy: -30.0)
|
||||
}
|
||||
}
|
||||
var updatedContentFrame = updatedImageFrame
|
||||
if isEmoji && emojiString == nil {
|
||||
updatedContentFrame = updatedContentFrame.insetBy(dx: -imageInset, dy: -imageInset)
|
||||
contextContentFrame = updatedContentFrame
|
||||
}
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: updatedImageFrame.maxY + 6.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.textNode.frame = labelFrame
|
||||
if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 {
|
||||
strongSelf.addSubnode(strongSelf.labelNode.textNode)
|
||||
}
|
||||
|
||||
let _ = labelApply(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
|
|
@ -1567,11 +1600,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
|
||||
attemptSynchronous: synchronousLoads
|
||||
))
|
||||
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.textNode.frame = labelFrame
|
||||
if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 {
|
||||
strongSelf.addSubnode(strongSelf.labelNode.textNode)
|
||||
}
|
||||
|
||||
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
||||
if let (offset, image) = backgroundMaskImage {
|
||||
|
|
@ -1604,21 +1632,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||
strongSelf.cachedMaskLabelBackgroundImage = (offset, image, labelRects)
|
||||
}
|
||||
|
||||
var updatedImageFrame: CGRect
|
||||
var contextContentFrame: CGRect
|
||||
if let _ = emojiString {
|
||||
updatedImageFrame = imageFrame
|
||||
contextContentFrame = updatedImageFrame.inset(by: UIEdgeInsets(top: 0.0, left: 0.0, bottom: -imageBottomPadding, right: 0.0))
|
||||
} else {
|
||||
updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0))
|
||||
contextContentFrame = updatedImageFrame
|
||||
}
|
||||
var updatedContentFrame = updatedImageFrame
|
||||
if isEmoji && emojiString == nil {
|
||||
updatedContentFrame = updatedContentFrame.insetBy(dx: -imageInset, dy: -imageInset)
|
||||
contextContentFrame = updatedContentFrame
|
||||
}
|
||||
|
||||
if let (_, textApply) = textLayoutAndApply {
|
||||
let placeholderColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper)
|
||||
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: placeholderColor, attemptSynchronous: synchronousLoads))
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
private var unlockButtonNode: ChatMessageUnlockMediaNode?
|
||||
private var mediaInfoNode: ChatMessageStarsMediaInfoNode?
|
||||
|
||||
private var summarizeButtonNode: ChatMessageShareButton?
|
||||
private var shareButtonNode: ChatMessageShareButton?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
|
@ -1222,6 +1223,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
}
|
||||
|
||||
if let summarizeButtonNode = strongSelf.summarizeButtonNode, summarizeButtonNode.frame.contains(point) {
|
||||
return .fail
|
||||
}
|
||||
|
||||
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return .fail
|
||||
}
|
||||
|
|
@ -1724,6 +1729,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
|
||||
|
||||
var needsShareButton = false
|
||||
var needsSummarizeButton = false
|
||||
|
||||
if incoming, case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind {
|
||||
needsShareButton = true
|
||||
|
|
@ -1755,6 +1761,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
needsShareButton = true
|
||||
needsSummarizeButton = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1789,6 +1796,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
loop: for media in item.message.media {
|
||||
if media is TelegramMediaAction {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
break loop
|
||||
} else if let media = media as? TelegramMediaFile, media.isInstantVideo {
|
||||
mayHaveSeparateCommentsButton = true
|
||||
|
|
@ -1801,12 +1809,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
if mayHaveSeparateCommentsButton && hasCommentButton(item: item) {
|
||||
} else {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isPreview {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
let isAd = item.content.firstMessage.adAttribute != nil
|
||||
if isAd {
|
||||
|
|
@ -1815,12 +1825,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
for attribute in item.content.firstMessage.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
}
|
||||
|
||||
if let subject = item.associatedData.subject, case .messageOptions = subject {
|
||||
needsShareButton = false
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
#else
|
||||
needsSummarizeButton = false
|
||||
#endif
|
||||
|
||||
var tmpWidth: CGFloat
|
||||
if allowFullWidth {
|
||||
|
|
@ -2717,7 +2734,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
hasReply = false
|
||||
}
|
||||
|
||||
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
|
||||
var isSummarized = false
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == "sum" {
|
||||
isSummarized = true
|
||||
hasReply = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil || isSummarized) {
|
||||
if headerSize.height.isZero {
|
||||
headerSize.height += 11.0
|
||||
} else {
|
||||
|
|
@ -2733,6 +2760,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: isSummarized,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
|
@ -3491,6 +3519,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
unlockButtonSizeAndApply: unlockButtonSizeApply,
|
||||
mediaInfoOrigin: mediaInfoOrigin?.offsetBy(dx: 0.0, dy: layoutInsets.top),
|
||||
mediaInfoSizeAndApply: mediaInfoSizeApply,
|
||||
needsSummarizeButton: needsSummarizeButton,
|
||||
needsShareButton: needsShareButton,
|
||||
shareButtonOffset: shareButtonOffset,
|
||||
avatarOffset: avatarOffset,
|
||||
|
|
@ -3556,6 +3585,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?),
|
||||
mediaInfoOrigin: CGPoint?,
|
||||
mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?),
|
||||
needsSummarizeButton: Bool,
|
||||
needsShareButton: Bool,
|
||||
shareButtonOffset: CGPoint?,
|
||||
avatarOffset: CGFloat?,
|
||||
|
|
@ -4787,6 +4817,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
})
|
||||
}
|
||||
|
||||
if needsSummarizeButton {
|
||||
if strongSelf.summarizeButtonNode == nil {
|
||||
let summarizeButtonNode = ChatMessageShareButton()
|
||||
strongSelf.summarizeButtonNode = summarizeButtonNode
|
||||
strongSelf.insertSubnode(summarizeButtonNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||
summarizeButtonNode.pressed = { [weak strongSelf] in
|
||||
strongSelf?.toggleSummarization()
|
||||
}
|
||||
}
|
||||
} else if let summarizeButtonNode = strongSelf.summarizeButtonNode {
|
||||
strongSelf.summarizeButtonNode = nil
|
||||
summarizeButtonNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if needsShareButton {
|
||||
if strongSelf.shareButtonNode == nil {
|
||||
let shareButtonNode = ChatMessageShareButton()
|
||||
|
|
@ -4960,6 +5004,33 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
}
|
||||
if let summarizeButtonNode = strongSelf.summarizeButtonNode {
|
||||
let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true)
|
||||
|
||||
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
|
||||
if strongSelf.shareButtonNode != nil {
|
||||
buttonFrame.origin.y -= buttonSize.width + 10.0
|
||||
}
|
||||
|
||||
if let shareButtonOffset = shareButtonOffset {
|
||||
if incoming {
|
||||
buttonFrame.origin.x = shareButtonOffset.x
|
||||
}
|
||||
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
|
||||
} else if !disablesComments {
|
||||
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
|
||||
}
|
||||
|
||||
if isSidePanelOpen {
|
||||
buttonFrame.origin.x -= buttonFrame.width * 0.5
|
||||
buttonFrame.origin.y += buttonFrame.height * 0.5
|
||||
}
|
||||
|
||||
animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
|
||||
animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
|
||||
animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
|
||||
}
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
|
||||
|
||||
|
|
@ -4994,6 +5065,33 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
strongSelf.backgroundFrameTransition = nil
|
||||
}*/
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
if let summarizeButtonNode = strongSelf.summarizeButtonNode {
|
||||
let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true)
|
||||
|
||||
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
|
||||
if strongSelf.shareButtonNode != nil {
|
||||
buttonFrame.origin.y -= buttonSize.width + 10.0
|
||||
}
|
||||
|
||||
if let shareButtonOffset = shareButtonOffset {
|
||||
if incoming {
|
||||
buttonFrame.origin.x = shareButtonOffset.x
|
||||
}
|
||||
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
|
||||
} else if !disablesComments {
|
||||
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
|
||||
}
|
||||
|
||||
if isSidePanelOpen {
|
||||
buttonFrame.origin.x -= buttonFrame.width * 0.5
|
||||
buttonFrame.origin.y += buttonFrame.height * 0.5
|
||||
}
|
||||
|
||||
animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
|
||||
animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
|
||||
animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
|
||||
}
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
|
||||
|
||||
|
|
@ -5275,6 +5373,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
}
|
||||
} else if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) {
|
||||
if let item = self.item {
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
return .action(InternalBubbleTapAction.Action({ [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.toggleSummarization()
|
||||
}))
|
||||
}
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute {
|
||||
if let threadId = item.message.threadId, Int32(clamping: threadId) == attribute.messageId.id, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute {
|
||||
|
|
@ -5824,6 +5931,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
return boostButtonNode.view
|
||||
}
|
||||
|
||||
if let summarizeButtonNode = self.summarizeButtonNode, summarizeButtonNode.frame.contains(point) {
|
||||
return summarizeButtonNode.view.hitTest(self.view.convert(point, to: summarizeButtonNode.view), with: event)
|
||||
}
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event)
|
||||
}
|
||||
|
|
@ -6516,6 +6627,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
container.updateAbsoluteRect(containerFrame, within: containerSize)
|
||||
}
|
||||
|
||||
if let summarizeButtonNode = self.summarizeButtonNode {
|
||||
var summarizeButtonNodeFrame = summarizeButtonNode.frame
|
||||
summarizeButtonNodeFrame.origin.x += rect.minX
|
||||
summarizeButtonNodeFrame.origin.y += rect.minY
|
||||
|
||||
summarizeButtonNode.updateAbsoluteRect(summarizeButtonNodeFrame, within: containerSize)
|
||||
}
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode {
|
||||
var shareButtonNodeFrame = shareButtonNode.frame
|
||||
shareButtonNodeFrame.origin.x += rect.minX
|
||||
|
|
@ -6854,6 +6973,35 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||
self.updateVisibility(isScroll: false)
|
||||
}
|
||||
|
||||
private func toggleSummarization() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
item.controllerInteraction.summarizedMessageIds.remove(item.message.id)
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, true)
|
||||
} else {
|
||||
item.controllerInteraction.summarizedMessageIds.insert(item.message.id)
|
||||
|
||||
var needsSummarization = true
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TranslationMessageAttribute, attribute.toLang == "sum" {
|
||||
needsSummarization = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if needsSummarization {
|
||||
let _ = (item.context.engine.messages.translateMessages(messageIds: [item.message.id], fromLang: nil, toLang: "sum", enableLocalIfPossible: false)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, true)
|
||||
})
|
||||
} else {
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibility(isScroll: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ReactionButtonListComponent
|
|||
import ReactionImageComponent
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import TelegramStringFormatting
|
||||
|
||||
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
||||
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
||||
|
|
@ -197,6 +198,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
var messageEffect: AvailableMessageEffects.MessageEffect?
|
||||
var replyCount: Int
|
||||
var starsCount: Int64?
|
||||
var tonAmount: Int64?
|
||||
var isPinned: Bool
|
||||
var hasAutoremove: Bool
|
||||
var canViewReactionList: Bool
|
||||
|
|
@ -222,6 +224,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
messageEffect: AvailableMessageEffects.MessageEffect?,
|
||||
replyCount: Int,
|
||||
starsCount: Int64?,
|
||||
tonAmount: Int64? = nil,
|
||||
isPinned: Bool,
|
||||
hasAutoremove: Bool,
|
||||
canViewReactionList: Bool,
|
||||
|
|
@ -246,6 +249,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
self.messageEffect = messageEffect
|
||||
self.replyCount = replyCount
|
||||
self.starsCount = starsCount
|
||||
self.tonAmount = tonAmount
|
||||
self.isPinned = isPinned
|
||||
self.hasAutoremove = hasAutoremove
|
||||
self.canViewReactionList = canViewReactionList
|
||||
|
|
@ -417,8 +421,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
} else if arguments.isPinned {
|
||||
repliesImage = graphics.incomingDateAndStatusPinnedIcon
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.incomingDateAndStatusStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.incomingDateAndStatusTonIcon
|
||||
}
|
||||
case let .BubbleOutgoing(status):
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
|
|
@ -438,6 +444,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.outgoingDateAndStatusStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.outgoingDateAndStatusTonIcon
|
||||
}
|
||||
case .ImageIncoming:
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
|
||||
|
|
@ -457,6 +465,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaTonIcon
|
||||
}
|
||||
case let .ImageOutgoing(status):
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
|
||||
|
|
@ -477,6 +487,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaTonIcon
|
||||
}
|
||||
case .FreeIncoming:
|
||||
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
||||
|
|
@ -498,6 +510,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.freeStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.freeTonIcon
|
||||
}
|
||||
case let .FreeOutgoing(status):
|
||||
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
||||
|
|
@ -519,6 +533,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.freeStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.freeTonIcon
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -717,7 +733,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
|
||||
let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
||||
if arguments.starsCount != nil {
|
||||
if arguments.starsCount != nil || arguments.tonAmount != nil {
|
||||
reactionInset += 3.0
|
||||
}
|
||||
replyCountLayoutAndApply = layoutAndApply
|
||||
|
|
@ -735,6 +751,11 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||
countString = "\(starsCount)"
|
||||
}
|
||||
|
||||
let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
||||
starsCountLayoutAndApply = layoutAndApply
|
||||
} else if let tonAmount = arguments.tonAmount, tonAmount > 0 {
|
||||
let countString = formatTonAmountText(tonAmount, dateTimeFormat: arguments.presentationData.dateTimeFormat)
|
||||
let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
||||
starsCountLayoutAndApply = layoutAndApply
|
||||
|
|
|
|||
|
|
@ -530,6 +530,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
|
|||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
|
|
|||
|
|
@ -404,6 +404,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
|
|||
override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = width
|
||||
|
||||
let panelHeight: CGFloat = 66.0
|
||||
let panelHeight: CGFloat = 64.0
|
||||
|
||||
guard let item = self.item else {
|
||||
return panelHeight
|
||||
|
|
@ -113,19 +113,19 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
|
|||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let leftInset: CGFloat = 14.0
|
||||
let rightInset: CGFloat = 14.0
|
||||
let avatarSize: CGFloat = 38.0
|
||||
let leftInset: CGFloat = 12.0
|
||||
let rightInset: CGFloat = 12.0
|
||||
let avatarSize: CGFloat = 40.0
|
||||
let avatarTextSpacing: CGFloat = 10.0
|
||||
let buttonSpacing: CGFloat = 14.0
|
||||
let titleTextSpacing: CGFloat = 0.0
|
||||
let titleTextSpacing: CGFloat = 1.0
|
||||
|
||||
let maxTextWidth: CGFloat = width - leftInset - avatarTextSpacing - rightInset - avatarSize * 2.0 - buttonSpacing - avatarTextSpacing
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
||||
|
|
@ -134,7 +134,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
|
|||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
||||
|
|
|
|||
|
|
@ -381,11 +381,11 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||
var applyImage: (() -> Void)?
|
||||
if let imageDimensions = imageDimensions {
|
||||
let boundingSize = CGSize(width: 55.0, height: 55.0)
|
||||
var radius: CGFloat = 6.0
|
||||
var radius: CGFloat = 20.0
|
||||
if isRound {
|
||||
radius = floor(boundingSize.width / 2.0)
|
||||
}
|
||||
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius, curve: isRound ? .circular : .continuous), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
}
|
||||
|
||||
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
|
|
@ -424,16 +424,16 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||
let compact = self.compact ?? false
|
||||
|
||||
let panelHeight: CGFloat = compact ? 64.0 : 74.0
|
||||
let imageSize: CGSize = compact ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 54.0, height: 54.0)
|
||||
let imageSpacing: CGFloat = compact ? 19.0 : 23.0
|
||||
let imageSize: CGSize = compact ? CGSize(width: 40.0, height: 40.0) : CGSize(width: 54.0, height: 54.0)
|
||||
let imageSpacing: CGFloat = compact ? 22.0 : 23.0
|
||||
let leftInset: CGFloat = imageSize.width + imageSpacing
|
||||
var rightInset: CGFloat = 8.0
|
||||
var rightInset: CGFloat = 10.0
|
||||
|
||||
if !self.imageNode.isHidden {
|
||||
rightInset += imageSize.width + 8.0
|
||||
rightInset += imageSize.width + 10.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 10.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 12.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
|
||||
var titleInset: CGFloat = 0.0
|
||||
if let image = self.titleIconNode.image {
|
||||
|
|
@ -465,7 +465,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||
|
||||
let textSpacing: CGFloat = 1.0
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
if let image = self.titleIconNode.image {
|
||||
|
|
@ -475,7 +475,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)
|
||||
transition.updateFrame(node: self.textNode.textNode, frame: textFrame)
|
||||
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 12.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
|
||||
if !textLayout.spoilers.isEmpty, let item = self.item {
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ swift_library(
|
|||
"//submodules/CheckNode",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
|
||||
"//submodules/Markdown",
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -16,309 +16,10 @@ import CheckNode
|
|||
import Markdown
|
||||
import TextFormat
|
||||
import StarsBalanceOverlayComponent
|
||||
import AlertComponent
|
||||
import AlertCheckComponent
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let boldTextFont = Font.semibold(13.0)
|
||||
|
||||
private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment)
|
||||
}
|
||||
|
||||
private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGestureRecognizerDelegate {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
private let optionText: String?
|
||||
private let alignment: TextAlertContentActionLayout
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private let checkNode: InteractiveCheckNode
|
||||
private let checkLabelNode: ImmediateTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
var dontAskAgain: Bool = false {
|
||||
didSet {
|
||||
self.checkNode.setSelected(self.dontAskAgain, animated: true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var openTerms: () -> Void = {}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.optionText = optionText
|
||||
self.alignment = alignment
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.textAlignment = .center
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.lineSpacing = 0.1
|
||||
self.textNode.textAlignment = .center
|
||||
|
||||
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
self.checkLabelNode = ImmediateTextNode()
|
||||
self.checkLabelNode.maximumNumberOfLines = 4
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
if let _ = optionText {
|
||||
self.addSubnode(self.checkNode)
|
||||
self.addSubnode(self.checkLabelNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.checkNode.valueChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dontAskAgain = !strongSelf.dontAskAgain
|
||||
}
|
||||
}
|
||||
|
||||
self.checkLabelNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.checkLabelNode.tapAttributeAction = { [weak self] attributes, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
self?.openTerms()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))
|
||||
tapGesture.delegate = self.wrappedGestureRecognizerDelegate
|
||||
self.view.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let location = gestureRecognizer.location(in: self.checkLabelNode.view)
|
||||
if self.checkLabelNode.bounds.contains(location) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let (_, attributes) = self.checkLabelNode.attributesAtPoint(self.view.convert(point, to: self.checkLabelNode.view)) {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
self.dontAskAgain = !self.dontAskAgain
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
|
||||
|
||||
self.checkLabelNode.attributedText = parseMarkdownIntoAttributedString(
|
||||
self.optionText ?? "",
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
|
||||
linkAttribute: { _ in
|
||||
return nil
|
||||
}
|
||||
)
|
||||
)
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
var entriesHeight: CGFloat = 0.0
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height
|
||||
|
||||
if self.checkLabelNode.supernode != nil {
|
||||
origin.y += 21.0
|
||||
entriesHeight += 21.0
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
|
||||
|
||||
let spacing: CGFloat = 12.0
|
||||
let acceptTermsSize = self.checkLabelNode.updateLayout(condensedSize)
|
||||
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
|
||||
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
|
||||
|
||||
transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
|
||||
transition.updateFrame(node: self.checkLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
|
||||
origin.y += acceptTermsSize.height
|
||||
entriesHeight += acceptTermsSize.height
|
||||
origin.y += 21.0
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = self.alignment
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
let contentWidth = max(size.width, minActionsWidth)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatMessagePaymentAlertController: AlertController {
|
||||
public class ChatMessagePaymentAlertController: AlertScreen {
|
||||
private let context: AccountContext?
|
||||
private let presentationData: PresentationData
|
||||
private weak var parentNavigationController: NavigationController?
|
||||
|
|
@ -327,29 +28,26 @@ public class ChatMessagePaymentAlertController: AlertController {
|
|||
private let animateBalanceOverlay: Bool
|
||||
|
||||
private var didUpdateCurrency = false
|
||||
public var currency: CurrencyAmount.Currency {
|
||||
didSet {
|
||||
self.didUpdateCurrency = true
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var initialCurrency: CurrencyAmount.Currency?
|
||||
public var currency: CurrencyAmount.Currency?
|
||||
private var currencyDisposable: Disposable?
|
||||
|
||||
private let balance = ComponentView<Empty>()
|
||||
|
||||
private var didAppear = false
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
|
||||
public init(
|
||||
context: AccountContext?,
|
||||
presentationData: PresentationData,
|
||||
contentNode: AlertContentNode,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
configuration: Configuration = AlertScreen.Configuration(),
|
||||
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
|
||||
actionsSignal: Signal<[AlertScreen.Action], NoError>,
|
||||
navigationController: NavigationController?,
|
||||
chatPeerId: EnginePeer.Id,
|
||||
showBalance: Bool = true,
|
||||
currency: CurrencyAmount.Currency = .stars,
|
||||
currencySignal: Signal<CurrencyAmount.Currency, NoError> = .single(.stars),
|
||||
animateBalanceOverlay: Bool = true
|
||||
) {
|
||||
self.context = context
|
||||
|
|
@ -357,31 +55,84 @@ public class ChatMessagePaymentAlertController: AlertController {
|
|||
self.parentNavigationController = navigationController
|
||||
self.chatPeerId = chatPeerId
|
||||
self.showBalance = showBalance
|
||||
self.currency = currency
|
||||
self.animateBalanceOverlay = animateBalanceOverlay
|
||||
|
||||
super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData))
|
||||
}
|
||||
|
||||
super.init(
|
||||
configuration: configuration,
|
||||
contentSignal: contentSignal,
|
||||
actionsSignal: actionsSignal,
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
|
||||
self.willDismiss = { [weak self] in
|
||||
self.currencyDisposable = (currencySignal
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).start(next: { [weak self] currency in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.animateOut()
|
||||
}
|
||||
if self.currency == nil {
|
||||
self.initialCurrency = currency
|
||||
}
|
||||
self.currency = currency
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
context: AccountContext?,
|
||||
presentationData: PresentationData,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
configuration: Configuration = AlertScreen.Configuration(),
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [AlertScreen.Action],
|
||||
navigationController: NavigationController?,
|
||||
chatPeerId: EnginePeer.Id,
|
||||
showBalance: Bool = true,
|
||||
currency: CurrencyAmount.Currency = .stars,
|
||||
animateBalanceOverlay: Bool = true
|
||||
) {
|
||||
self.init(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
configuration: configuration,
|
||||
contentSignal: .single(content),
|
||||
actionsSignal: .single(actions),
|
||||
navigationController: navigationController,
|
||||
chatPeerId: chatPeerId,
|
||||
showBalance: showBalance,
|
||||
currencySignal: .single(currency),
|
||||
animateBalanceOverlay: animateBalanceOverlay
|
||||
)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
super.dismiss(completion: completion)
|
||||
|
||||
self.animateOut()
|
||||
}
|
||||
|
||||
private func animateOut() {
|
||||
if !self.animateBalanceOverlay {
|
||||
if self.currency == .ton && self.didUpdateCurrency {
|
||||
if case .ton = self.currency, let initialCurrency, initialCurrency != self.currency {
|
||||
self.currency = .stars
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
Queue.mainQueue().after(0.39, {
|
||||
|
||||
})
|
||||
} else {
|
||||
if let view = self.balance.view {
|
||||
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
|
||||
|
|
@ -389,18 +140,10 @@ public class ChatMessagePaymentAlertController: AlertController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func dismissAnimated() {
|
||||
super.dismissAnimated()
|
||||
|
||||
self.animateOut()
|
||||
}
|
||||
|
||||
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
|
||||
if !self.didAppear {
|
||||
self.didAppear = true
|
||||
if !layout.metrics.isTablet && layout.size.width > layout.size.height {
|
||||
|
|
@ -410,7 +153,7 @@ public class ChatMessagePaymentAlertController: AlertController {
|
|||
}
|
||||
}
|
||||
|
||||
if let context = self.context, let _ = self.parentNavigationController, self.showBalance {
|
||||
if let context = self.context, let _ = self.parentNavigationController, self.showBalance, let currency = self.currency {
|
||||
let insets = layout.insets(options: .statusBar)
|
||||
var balanceTransition = ComponentTransition(transition)
|
||||
if self.balance.view == nil {
|
||||
|
|
@ -424,12 +167,12 @@ public class ChatMessagePaymentAlertController: AlertController {
|
|||
context: context,
|
||||
peerId: self.chatPeerId.namespace == Namespaces.Peer.CloudChannel ? self.chatPeerId : context.account.peerId,
|
||||
theme: self.presentationData.theme,
|
||||
currency: self.currency,
|
||||
currency: currency,
|
||||
action: { [weak self] in
|
||||
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController else {
|
||||
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController, let currency = self.currency else {
|
||||
return
|
||||
}
|
||||
switch self.currency {
|
||||
switch currency {
|
||||
case .stars:
|
||||
let _ = (context.engine.payments.starsTopUpOptions()
|
||||
|> take(1)
|
||||
|
|
@ -452,7 +195,7 @@ public class ChatMessagePaymentAlertController: AlertController {
|
|||
}
|
||||
context.sharedContext.applicationBindings.openUrl(fragmentUrl)
|
||||
}
|
||||
self.dismissAnimated()
|
||||
self.dismiss(completion: nil)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
@ -486,57 +229,66 @@ public func chatMessagePaymentAlertController(
|
|||
hasCheck: Bool = true,
|
||||
navigationController: NavigationController?,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData = updatedPresentationData?.initial ?? presentationData
|
||||
) -> ViewController {
|
||||
let strings = presentationData.strings
|
||||
|
||||
var completionImpl: (() -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let title = presentationData.strings.Chat_PaidMessage_Confirm_Title
|
||||
let actionTitle = presentationData.strings.Chat_PaidMessage_Confirm_PayForMessage(count)
|
||||
let messagesString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Messages(count)
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: {
|
||||
completionImpl?()
|
||||
dismissImpl?()
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
})]
|
||||
|
||||
let messagesString = strings.Chat_PaidMessage_Confirm_Text_Messages(count)
|
||||
let text: String
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
let amountString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
|
||||
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
let amountString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
|
||||
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
if case let .channel(channel) = peer.chatOrMonoforumMainPeer, case .broadcast = channel.info {
|
||||
text = presentationData.strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
|
||||
text = strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
|
||||
} else {
|
||||
text = presentationData.strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
|
||||
text = strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
|
||||
}
|
||||
} else {
|
||||
let amount = totalAmount ?? amount
|
||||
let usersString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
|
||||
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
text = presentationData.strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
|
||||
let usersString = strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
|
||||
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
text = strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
|
||||
}
|
||||
|
||||
let checkState = AlertCheckComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Chat_PaidMessage_Confirm_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
if hasCheck {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "check",
|
||||
component: AnyComponent(
|
||||
AlertCheckComponent(title: strings.Chat_PaidMessage_Confirm_DontAskAgain, initialValue: false, externalState: checkState)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
let optionText = hasCheck ? presentationData.strings.Chat_PaidMessage_Confirm_DontAskAgain : nil
|
||||
|
||||
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical)
|
||||
|
||||
completionImpl = { [weak contentNode] in
|
||||
guard let contentNode else {
|
||||
return
|
||||
}
|
||||
completion(contentNode.dontAskAgain)
|
||||
}
|
||||
|
||||
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: context?.account.peerId ?? peers[0].peerId)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
return controller
|
||||
let alertController = ChatMessagePaymentAlertController(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Chat_PaidMessage_Confirm_PayForMessage(count), type: .default, action: {
|
||||
completion(checkState.value)
|
||||
}),
|
||||
.init(title: strings.Common_Cancel)
|
||||
],
|
||||
navigationController: navigationController,
|
||||
chatPeerId: context?.account.peerId ?? peers[0].peerId
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
||||
public func chatMessageRemovePaymentAlertController(
|
||||
|
|
@ -548,26 +300,9 @@ public func chatMessageRemovePaymentAlertController(
|
|||
amount: StarsAmount?,
|
||||
navigationController: NavigationController?,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData = updatedPresentationData?.initial ?? presentationData
|
||||
) -> ViewController {
|
||||
let strings = presentationData.strings
|
||||
|
||||
var completionImpl: (() -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [
|
||||
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: strings.Chat_PaidMessage_RemoveFee_Yes, action: {
|
||||
completionImpl?()
|
||||
dismissImpl?()
|
||||
})
|
||||
]
|
||||
|
||||
let title = strings.Chat_PaidMessage_RemoveFee_Title
|
||||
|
||||
let text: String
|
||||
if case .user = chatPeer {
|
||||
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
|
||||
|
|
@ -576,21 +311,44 @@ public func chatMessageRemovePaymentAlertController(
|
|||
} else {
|
||||
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
|
||||
}
|
||||
|
||||
let checkState = AlertCheckComponent.ExternalState()
|
||||
|
||||
let optionText = amount.flatMap { strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: $0.value))).string }
|
||||
|
||||
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal)
|
||||
|
||||
completionImpl = { [weak contentNode] in
|
||||
guard let contentNode else {
|
||||
return
|
||||
}
|
||||
completion(contentNode.dontAskAgain)
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Chat_PaidMessage_RemoveFee_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
if let amount {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "check",
|
||||
component: AnyComponent(
|
||||
AlertCheckComponent(title: strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: amount.value))).string, initialValue: false, externalState: checkState)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: chatPeer.id)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
return controller
|
||||
let alertController = ChatMessagePaymentAlertController(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.Chat_PaidMessage_RemoveFee_Yes, type: .default, action: {
|
||||
completion(checkState.value)
|
||||
})
|
||||
],
|
||||
navigationController: navigationController,
|
||||
chatPeerId: chatPeer.id
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||
public let quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?
|
||||
public let todoItemId: Int32?
|
||||
public let story: StoryId?
|
||||
public let isSummarized: Bool
|
||||
public let parentMessage: Message
|
||||
public let constrainedSize: CGSize
|
||||
public let animationCache: AnimationCache?
|
||||
|
|
@ -101,6 +102,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||
quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?,
|
||||
todoItemId: Int32?,
|
||||
story: StoryId?,
|
||||
isSummarized: Bool,
|
||||
parentMessage: Message,
|
||||
constrainedSize: CGSize,
|
||||
animationCache: AnimationCache?,
|
||||
|
|
@ -116,6 +118,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||
self.quote = quote
|
||||
self.todoItemId = todoItemId
|
||||
self.story = story
|
||||
self.isSummarized = isSummarized
|
||||
self.parentMessage = parentMessage
|
||||
self.constrainedSize = constrainedSize
|
||||
self.animationCache = animationCache
|
||||
|
|
@ -437,7 +440,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
var textLeftInset: CGFloat = 0.0
|
||||
let messageText: NSAttributedString
|
||||
var messageText: NSAttributedString
|
||||
var todoItemCompleted: Bool?
|
||||
if let todoItemId = arguments.todoItemId, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
|
||||
messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
|
||||
|
|
@ -607,6 +610,12 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||
}
|
||||
adjustedConstrainedTextSize.width -= textLeftInset
|
||||
|
||||
if arguments.isSummarized {
|
||||
//TODO:localize
|
||||
titleString = NSAttributedString(string: "AI Summary", font: titleFont, textColor: titleColor)
|
||||
messageText = NSAttributedString(string: "Tap to see original text", font: textFont, textColor: titleColor)
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
|
||||
if isExpiredStory || isStory {
|
||||
contrainedTextSize.width -= 26.0
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public class ChatMessageShareButton: ASDisplayNode {
|
|||
private var theme: PresentationTheme?
|
||||
private var isReplies: Bool = false
|
||||
private var hasMore: Bool = false
|
||||
private var isExpand: Bool = false
|
||||
|
||||
private var textNode: ImmediateTextNode?
|
||||
|
||||
|
|
@ -103,7 +104,7 @@ public class ChatMessageShareButton: ASDisplayNode {
|
|||
self.morePressed?()
|
||||
}
|
||||
|
||||
public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize {
|
||||
public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false, isSummarize: Bool = false) -> CGSize {
|
||||
var isReplies = false
|
||||
var isNavigate = false
|
||||
var replyCount = 0
|
||||
|
|
@ -134,15 +135,27 @@ public class ChatMessageShareButton: ASDisplayNode {
|
|||
hasMore = true
|
||||
}
|
||||
|
||||
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore {
|
||||
var isExpand = false
|
||||
if controllerInteraction.summarizedMessageIds.contains(message.id) {
|
||||
isExpand = true
|
||||
}
|
||||
|
||||
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore || self.isExpand != isExpand {
|
||||
self.theme = presentationData.theme.theme
|
||||
self.isReplies = isReplies
|
||||
self.hasMore = hasMore
|
||||
self.isExpand = isExpand
|
||||
|
||||
var updatedIconImage: UIImage?
|
||||
var updatedBottomIconImage: UIImage?
|
||||
var updatedIconOffset = CGPoint()
|
||||
if let _ = message.adAttribute {
|
||||
if isSummarize {
|
||||
if isExpand {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeExpandButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
} else {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeCollapseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
}
|
||||
} else if let _ = message.adAttribute {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeCloseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
updatedIconOffset = CGPoint(x: UIScreenPixel, y: UIScreenPixel)
|
||||
|
||||
|
|
|
|||
|
|
@ -777,6 +777,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue