Various improvements
This commit is contained in:
parent
9c0e6f6151
commit
a0a2f9f6bf
10 changed files with 348 additions and 145 deletions
|
|
@ -319,6 +319,7 @@ public class CheckLayer: CALayer {
|
|||
super.init()
|
||||
|
||||
self.isOpaque = false
|
||||
self.rasterizationScale = UIScreenScale
|
||||
}
|
||||
|
||||
public override init(layer: Any) {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
@ -87,6 +89,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
@ -104,6 +108,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
@ -124,6 +130,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
|||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||
return .single(.purchaseAvailable)
|
||||
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||
return .single(.taken)
|
||||
} else {
|
||||
return .single(.invalid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
private let counterTextNode: ImmediateTextNode
|
||||
|
||||
private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)?
|
||||
private var heightDependentAiButtonAlpha: CGFloat = 0.0
|
||||
|
||||
public let menuButton: HighlightTrackingButtonNode
|
||||
private let menuButtonBackgroundView: GlassBackgroundView
|
||||
|
|
@ -3558,7 +3559,15 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
transition.updateFrame(view: aiButton.button, frame: aiButtonFrame)
|
||||
transition.updateFrame(view: aiButton.icon, frame: image.size.centered(in: aiButtonFrame))
|
||||
}
|
||||
let aiButtonAlpha: CGFloat = actualTextFieldFrame.height >= 70.0 ? 1.0 : 0.0
|
||||
var aiButtonAlpha: CGFloat = actualTextFieldFrame.height >= 70.0 ? 1.0 : 0.0
|
||||
self.heightDependentAiButtonAlpha = aiButtonAlpha
|
||||
var inputHasText = false
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
||||
inputHasText = true
|
||||
}
|
||||
if !inputHasText {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha)
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
||||
} else if let aiButton = self.aiButton {
|
||||
|
|
@ -3571,6 +3580,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 0.0, completion: { [weak aiButtonIconView] _ in
|
||||
aiButtonIconView?.removeFromSuperview()
|
||||
})
|
||||
self.heightDependentAiButtonAlpha = 0.0
|
||||
}
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight + 64.0))
|
||||
|
|
@ -4385,6 +4395,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
|||
}
|
||||
}
|
||||
|
||||
if let aiButton = self.aiButton {
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
var aiButtonAlpha: CGFloat = self.heightDependentAiButtonAlpha
|
||||
if !inputHasText {
|
||||
aiButtonAlpha = 0.0
|
||||
}
|
||||
transition.updateAlpha(layer: aiButton.button.layer, alpha: aiButtonAlpha)
|
||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
||||
}
|
||||
|
||||
self.updateTextHeight(animated: animated)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ final class CreateBotContentComponent: Component {
|
|||
final class ExternalState {
|
||||
var name: String = ""
|
||||
var username: String = ""
|
||||
var usernameIsChecked: Bool = false
|
||||
|
||||
init() {
|
||||
}
|
||||
|
|
@ -54,6 +55,13 @@ final class CreateBotContentComponent: Component {
|
|||
static func ==(lhs: CreateBotContentComponent, rhs: CreateBotContentComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private enum UsernameCheckingStatus {
|
||||
case checking
|
||||
case valid
|
||||
case invalid
|
||||
case taken
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: CreateBotContentComponent?
|
||||
|
|
@ -67,7 +75,19 @@ final class CreateBotContentComponent: Component {
|
|||
private let usernameSection = ComponentView<Empty>()
|
||||
|
||||
private let usernameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
private let usernameInputTag = ListMultilineTextFieldItemComponent.Tag()
|
||||
private let nameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
private let nameInputTag = ListMultilineTextFieldItemComponent.Tag()
|
||||
|
||||
private var usernameCheckingStatus: (username: String, status: UsernameCheckingStatus)? {
|
||||
didSet {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.externalState.usernameIsChecked = self.usernameCheckingStatus?.status == .valid
|
||||
}
|
||||
}
|
||||
private var usernameCheckingDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -77,6 +97,11 @@ final class CreateBotContentComponent: Component {
|
|||
return
|
||||
}
|
||||
component.externalState.username = self.usernameInputState.text.string
|
||||
|
||||
self.inputUsernameUpdated()
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.nameInputState.updated = { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
|
|
@ -89,6 +114,47 @@ final class CreateBotContentComponent: Component {
|
|||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.usernameCheckingDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func inputUsernameUpdated() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let username = self.usernameInputState.text.string.lowercased() + "bot"
|
||||
if let usernameCheckingStatus = self.usernameCheckingStatus, usernameCheckingStatus.username == username {
|
||||
return
|
||||
}
|
||||
self.usernameCheckingDisposable?.dispose()
|
||||
self.usernameCheckingDisposable = nil
|
||||
|
||||
guard case .success = CreateBotSheetComponent.View.validatedUsername(inputUsername: username) else {
|
||||
self.usernameCheckingStatus = (username, .invalid)
|
||||
return
|
||||
}
|
||||
|
||||
self.usernameCheckingStatus = (username, .checking)
|
||||
self.usernameCheckingDisposable = (component.context.engine.peers.addressNameAvailability(domain: .bot(component.parentPeer.id), name: username) |> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case .available:
|
||||
self.usernameCheckingStatus = (username, .valid)
|
||||
case .invalid:
|
||||
self.usernameCheckingStatus = (username, .invalid)
|
||||
case .purchaseAvailable:
|
||||
self.usernameCheckingStatus = (username, .invalid)
|
||||
case .taken:
|
||||
self.usernameCheckingStatus = (username, .taken)
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: CreateBotContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
|
|
@ -210,10 +276,24 @@ final class CreateBotContentComponent: Component {
|
|||
autocapitalizationType: .words,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 64,
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EditLabelComponent(theme: environment.theme, strings: environment.strings))), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)),
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(
|
||||
id: 0,
|
||||
component: AnyComponent(EditLabelComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
action: { [weak self] in
|
||||
guard let self, let itemView = self.nameSection.findTaggedView(tag: self.nameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||
return
|
||||
}
|
||||
itemView.activateInput()
|
||||
}
|
||||
))),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)
|
||||
),
|
||||
emptyLineHandling: .notAllowed,
|
||||
updated: { _ in },
|
||||
textUpdateTransition: .immediate
|
||||
textUpdateTransition: .immediate,
|
||||
tag: self.nameInputTag
|
||||
)))
|
||||
]
|
||||
)),
|
||||
|
|
@ -231,14 +311,58 @@ final class CreateBotContentComponent: Component {
|
|||
contentHeight += nameSectionSize.height + 22.0
|
||||
|
||||
var initialUsername = ""
|
||||
var botSuffix = "bot"
|
||||
if let value = component.initialUsername {
|
||||
if value.hasSuffix("bot") {
|
||||
if value.lowercased().hasSuffix("bot") {
|
||||
botSuffix = String(value[value.index(value.endIndex, offsetBy: -3)...])
|
||||
initialUsername = String(value[value.startIndex ..< value.index(value.endIndex, offsetBy: -3)])
|
||||
} else {
|
||||
initialUsername = value
|
||||
}
|
||||
}
|
||||
|
||||
let usernameFooterString: NSAttributedString
|
||||
switch CreateBotSheetComponent.View.validatedUsername(inputUsername: "\(self.usernameInputState.text.string)" + botSuffix) {
|
||||
case let .success(value):
|
||||
switch self.usernameCheckingStatus?.status ?? .valid {
|
||||
case .checking:
|
||||
usernameFooterString = NSAttributedString(
|
||||
string: "Checking...",
|
||||
font: Font.regular(13.0),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)
|
||||
case .invalid:
|
||||
let errorText = "You can only use **a-z**, **0-9** and underscores."
|
||||
usernameFooterString = parseMarkdownIntoAttributedString(errorText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemDestructiveColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
case .taken:
|
||||
let errorText = "This username is already taken."
|
||||
usernameFooterString = parseMarkdownIntoAttributedString(errorText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemDestructiveColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
case .valid:
|
||||
usernameFooterString = NSAttributedString(
|
||||
string: "Link: t.me/\(value)",
|
||||
font: Font.regular(13.0),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)
|
||||
}
|
||||
case let .failure(error):
|
||||
let errorText: String
|
||||
switch error {
|
||||
case .insufficientLength:
|
||||
errorText = "A username must have at least 5 characters."
|
||||
case .startsWithNumber:
|
||||
errorText = "A username can't start with a number"
|
||||
case .unsupportedCharacters:
|
||||
errorText = "You can only use **a-z**, **0-9** and underscores."
|
||||
}
|
||||
usernameFooterString = parseMarkdownIntoAttributedString(errorText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemDestructiveColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemDestructiveColor), linkAttribute: { contents in
|
||||
return ("URL", contents)
|
||||
}))
|
||||
}
|
||||
|
||||
let usernameSectionSize = self.usernameSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
|
|
@ -252,7 +376,10 @@ final class CreateBotContentComponent: Component {
|
|||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(usernameFooterString),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||
externalState: usernameInputState,
|
||||
|
|
@ -268,11 +395,25 @@ final class CreateBotContentComponent: Component {
|
|||
keyboardType: .asciiCapable,
|
||||
characterLimit: 32,
|
||||
prefix: NSAttributedString(string: "@", font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor),
|
||||
suffix: NSAttributedString(string: "bot", font: Font.regular(17.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EditLabelComponent(theme: environment.theme, strings: environment.strings))), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)),
|
||||
suffix: NSAttributedString(string: botSuffix, font: Font.regular(17.0), textColor: environment.theme.list.itemSecondaryTextColor),
|
||||
rightAccessory: ListMultilineTextFieldItemComponent.RightAccessory(component: AnyComponentWithIdentity(
|
||||
id: 0,
|
||||
component: AnyComponent(EditLabelComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
action: { [weak self] in
|
||||
guard let self, let itemView = self.nameSection.findTaggedView(tag: self.usernameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||
return
|
||||
}
|
||||
itemView.activateInput()
|
||||
}
|
||||
))),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)
|
||||
),
|
||||
emptyLineHandling: .notAllowed,
|
||||
updated: { _ in },
|
||||
textUpdateTransition: .immediate
|
||||
textUpdateTransition: .immediate,
|
||||
tag: self.usernameInputTag,
|
||||
)))
|
||||
]
|
||||
)),
|
||||
|
|
@ -294,6 +435,7 @@ final class CreateBotContentComponent: Component {
|
|||
|
||||
component.externalState.name = self.nameInputState.text.string
|
||||
component.externalState.username = self.usernameInputState.text.string
|
||||
component.externalState.usernameIsChecked = self.usernameCheckingStatus?.status == .valid
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
|
|
@ -316,7 +458,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
let initialUsername: String?
|
||||
let initialTitle: String?
|
||||
let openAutomatically: Bool
|
||||
let completion: (EnginePeer.Id) -> Void
|
||||
let completion: (EnginePeer.Id?) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -324,7 +466,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
initialUsername: String?,
|
||||
initialTitle: String?,
|
||||
openAutomatically: Bool,
|
||||
completion: @escaping (EnginePeer.Id) -> Void
|
||||
completion: @escaping (EnginePeer.Id?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.parentPeer = parentPeer
|
||||
|
|
@ -349,6 +491,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
|
||||
private var isCreating: Bool = false
|
||||
private var actionDisposable: Disposable?
|
||||
private var isCompleted: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -362,28 +505,94 @@ private final class CreateBotSheetComponent: Component {
|
|||
self.actionDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func validatedParams() -> (name: String, username: String)? {
|
||||
if self.contentExternalState.name.isEmpty {
|
||||
return nil
|
||||
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
|
||||
guard let component = self.component else {
|
||||
return true
|
||||
}
|
||||
if self.contentExternalState.username.isEmpty {
|
||||
return nil
|
||||
if self.isCreating {
|
||||
return false
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = presentationData
|
||||
let alertController = textAlertController(
|
||||
context: component.context,
|
||||
title: "Unsaved Changes",
|
||||
text: "You have not finished creating a bot.",
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: "Cancel", action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: "Discard", action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.isCompleted {
|
||||
self.isCompleted = true
|
||||
component.completion(nil)
|
||||
}
|
||||
let controller = self.environment?.controller
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller?() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
})
|
||||
]
|
||||
)
|
||||
self.environment?.controller()?.present(alertController, in: .window(.root))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
enum UsernameValidationError: Error {
|
||||
case insufficientLength
|
||||
case unsupportedCharacters
|
||||
case startsWithNumber
|
||||
}
|
||||
|
||||
static func validatedUsername(inputUsername: String) -> Result<String, UsernameValidationError> {
|
||||
var isUsernameValid = true
|
||||
var usernameCharacters = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
|
||||
usernameCharacters.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!)
|
||||
usernameCharacters.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
|
||||
usernameCharacters.insert("_")
|
||||
for c in self.contentExternalState.username.unicodeScalars {
|
||||
for c in inputUsername.unicodeScalars {
|
||||
if !usernameCharacters.contains(c) {
|
||||
isUsernameValid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isUsernameValid {
|
||||
return .failure(.unsupportedCharacters)
|
||||
}
|
||||
if let first = inputUsername.unicodeScalars.first {
|
||||
if CharacterSet.decimalDigits.contains(first) {
|
||||
return .failure(.startsWithNumber)
|
||||
}
|
||||
}
|
||||
if inputUsername.count < 5 {
|
||||
return .failure(.insufficientLength)
|
||||
}
|
||||
return .success(inputUsername)
|
||||
}
|
||||
|
||||
static func validatedParams(inputName: String, inputUsername: String) -> (name: String, username: String)? {
|
||||
if inputName.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return (self.contentExternalState.name, self.contentExternalState.username)
|
||||
guard case let .success(username) = validatedUsername(inputUsername: inputUsername) else {
|
||||
return nil
|
||||
}
|
||||
return (inputName, username)
|
||||
}
|
||||
|
||||
private func validatedParams() -> (name: String, username: String)? {
|
||||
if !self.contentExternalState.usernameIsChecked {
|
||||
return nil
|
||||
}
|
||||
return CreateBotSheetComponent.View.validatedParams(inputName: contentExternalState.name, inputUsername: self.contentExternalState.username)
|
||||
}
|
||||
|
||||
private func performCreateBot() {
|
||||
|
|
@ -413,6 +622,7 @@ private final class CreateBotSheetComponent: Component {
|
|||
return
|
||||
}
|
||||
let context = component.context
|
||||
self.isCompleted = true
|
||||
self.animateOut.invoke(Action { [weak controller, weak navigationController] _ in
|
||||
if let controller, let navigationController {
|
||||
controller.dismiss(completion: { [weak navigationController] in
|
||||
|
|
@ -466,8 +676,15 @@ private final class CreateBotSheetComponent: Component {
|
|||
let theme = environmentValue.theme
|
||||
|
||||
let dismiss: (Bool) -> Void = { [weak self] animated in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if !self.isCompleted {
|
||||
self.isCompleted = true
|
||||
component.completion(nil)
|
||||
}
|
||||
if animated {
|
||||
self?.animateOut.invoke(Action { _ in
|
||||
self.animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
|
|
@ -544,7 +761,17 @@ private final class CreateBotSheetComponent: Component {
|
|||
isCentered: environmentValue.metrics.widthClass == .regular,
|
||||
screenSize: availableSize,
|
||||
regularMetricsSize: nil,
|
||||
dismiss: { animated in
|
||||
dismiss: { [weak self] animated in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if animated {
|
||||
if !self.attemptNavigation(complete: {
|
||||
dismiss(animated)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
dismiss(animated)
|
||||
}
|
||||
)
|
||||
|
|
@ -581,7 +808,7 @@ public class CreateBotScreen: ViewControllerComponentContainer {
|
|||
initialUsername: String?,
|
||||
initialTitle: String?,
|
||||
openAutomatically: Bool,
|
||||
completion: @escaping (EnginePeer.Id) -> Void
|
||||
completion: @escaping (EnginePeer.Id?) -> Void
|
||||
) async {
|
||||
self.context = context
|
||||
|
||||
|
|
@ -609,6 +836,14 @@ public class CreateBotScreen: ViewControllerComponentContainer {
|
|||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.attemptNavigation = { [weak self] complete in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? CreateBotSheetComponent.View else {
|
||||
return true
|
||||
}
|
||||
|
||||
return componentView.attemptNavigation(complete: complete)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
|
|
@ -777,13 +1012,16 @@ private final class ActionButtonsComponent: Component {
|
|||
private final class EditLabelComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings
|
||||
strings: PresentationStrings,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: EditLabelComponent, rhs: EditLabelComponent) -> Bool {
|
||||
|
|
@ -805,12 +1043,23 @@ private final class EditLabelComponent: Component {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
component.action()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: EditLabelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
|
@ -846,6 +1095,7 @@ private final class EditLabelComponent: Component {
|
|||
)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
backgroundView.isUserInteractionEnabled = false
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
|
|
@ -853,6 +1103,7 @@ private final class EditLabelComponent: Component {
|
|||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||
case legacy
|
||||
}
|
||||
|
||||
public final class Tag {
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
||||
|
|
|
|||
|
|
@ -126,9 +126,11 @@ public extension PeerInfoScreenImpl {
|
|||
commit()
|
||||
}
|
||||
controller.videoCompletion = { [weak parentController] image, url, values, markup, commit in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
resultImage = image
|
||||
let _ = parentController
|
||||
updateProfileVideo(context: context, image: image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
commit()
|
||||
}
|
||||
parentController.push(controller)
|
||||
|
|
@ -193,7 +195,7 @@ public extension PeerInfoScreenImpl {
|
|||
case let .video(video, coverImage, values, _, _):
|
||||
if let coverImage {
|
||||
resultImage = coverImage
|
||||
updateProfileVideo(context: context, image: coverImage, video: video, values: values, markup: nil, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: coverImage, video: video, values: values, markup: nil, mode: mode, uploadStatus: uploadStatusPromise)
|
||||
}
|
||||
commit({})
|
||||
default:
|
||||
|
|
@ -283,52 +285,24 @@ public extension PeerInfoScreenImpl {
|
|||
|
||||
if case .complete = result {
|
||||
dismissStatus?()
|
||||
|
||||
/*let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
if let strongSelf = self, let peer {
|
||||
switch mode {
|
||||
case .fallback:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
case .custom:
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
|
||||
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
|
||||
case .suggest:
|
||||
if let navigationController = (strongSelf.navigationController as? NavigationController) {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
|
||||
}))
|
||||
}
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})*/
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
static func updateProfileVideo(context: AccountContext, image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?) {
|
||||
updateProfileVideo(context: context, image: image, video: video as? MediaEditorScreenImpl.MediaResult.VideoResult, values: values as? MediaEditorValues, markup: markup, mode: .generic, uploadStatus: nil)
|
||||
static func updateProfileVideo(parentController: ViewController, context: AccountContext, peer: EnginePeer, image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?) {
|
||||
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: image, video: video as? MediaEditorScreenImpl.MediaResult.VideoResult, values: values as? MediaEditorValues, markup: markup, mode: .generic, uploadStatus: nil)
|
||||
}
|
||||
|
||||
static func updateProfileVideo(context: AccountContext, image: UIImage, video: MediaEditorScreenImpl.MediaResult.VideoResult?, values: MediaEditorValues?, markup: UploadPeerPhotoMarkup?, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
|
||||
/*var uploadVideo = true
|
||||
static func updateProfileVideo(parentController: ViewController, context: AccountContext, peer: EnginePeer, image: UIImage, video: MediaEditorScreenImpl.MediaResult.VideoResult?, values: MediaEditorValues?, markup: UploadPeerPhotoMarkup?, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise<PeerInfoAvatarUploadStatus>?) {
|
||||
var uploadVideo = true
|
||||
if let _ = markup {
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue {
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue {
|
||||
uploadVideo = true
|
||||
} else {
|
||||
uploadVideo = false
|
||||
}
|
||||
}
|
||||
guard let photoResource = self.setupProfilePhotoUpload(image: image, mode: mode, indefiniteProgress: !uploadVideo) else {
|
||||
guard let photoResource = sharedSetupProfilePhotoUpload(context: context, image: image, mode: mode) else {
|
||||
uploadStatus?.set(.single(.done))
|
||||
return
|
||||
}
|
||||
|
|
@ -338,8 +312,8 @@ public extension PeerInfoScreenImpl {
|
|||
videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0)
|
||||
}
|
||||
|
||||
let account = self.context.account
|
||||
let context = self.context
|
||||
let account = context.account
|
||||
let context = context
|
||||
|
||||
let videoResource: Signal<TelegramMediaResource?, UploadPeerPhotoError>
|
||||
if uploadVideo, let video, let values {
|
||||
|
|
@ -388,10 +362,7 @@ public extension PeerInfoScreenImpl {
|
|||
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
|
||||
let videoExport = MediaEditorVideoExport(postbox: context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: tempFile.path, textScale: 2.0)
|
||||
let _ = (videoExport.status
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] status in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|> deliverOnMainQueue).startStandalone(next: { status in
|
||||
switch status {
|
||||
case .completed:
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) {
|
||||
|
|
@ -401,11 +372,8 @@ public extension PeerInfoScreenImpl {
|
|||
subscriber.putCompletion()
|
||||
}
|
||||
EngineTempBox.shared.dispose(tempFile)
|
||||
case let .progress(progress):
|
||||
Queue.mainQueue().async {
|
||||
self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(progress * 0.45)))
|
||||
self.requestLayout(transition: .immediate)
|
||||
}
|
||||
case .progress:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -422,100 +390,41 @@ public extension PeerInfoScreenImpl {
|
|||
}
|
||||
|
||||
var dismissStatus: (() -> Void)?
|
||||
if [.suggest, .fallback, .accept].contains(mode) {
|
||||
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in
|
||||
self?.controllerNode.updateAvatarDisposable.set(nil)
|
||||
if "".isEmpty {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
dismissStatus?()
|
||||
}))
|
||||
dismissStatus = { [weak statusController] in
|
||||
statusController?.dismiss()
|
||||
}
|
||||
if let topController = self.navigationController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else if let topController = self.parentController?.topViewController as? ViewController {
|
||||
if let topController = parentController.navigationController?.topViewController as? ViewController {
|
||||
topController.presentInGlobalOverlay(statusController)
|
||||
} else {
|
||||
self.presentInGlobalOverlay(statusController)
|
||||
parentController.presentInGlobalOverlay(statusController)
|
||||
}
|
||||
}
|
||||
|
||||
let peerId = self.peerId
|
||||
let isSettings = self.isSettings
|
||||
let isMyProfile = self.isMyProfile
|
||||
self.controllerNode.updateAvatarDisposable.set((videoResource
|
||||
let peerId = peer.id
|
||||
let updateAvatarDisposable = MetaDisposable()
|
||||
updateAvatarDisposable.set((videoResource
|
||||
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
|
||||
if isSettings || isMyProfile {
|
||||
if case .fallback = mode {
|
||||
return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
} else {
|
||||
return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
}
|
||||
} else if case .custom = mode {
|
||||
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
} else if case .suggest = mode {
|
||||
return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
} else {
|
||||
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
}
|
||||
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in
|
||||
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||
})
|
||||
}
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||
switch result {
|
||||
case .complete:
|
||||
uploadStatus?.set(.single(.done))
|
||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
|
||||
case let .progress(value):
|
||||
uploadStatus?.set(.single(.progress(value)))
|
||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(0.45 + value * 0.55)))
|
||||
}
|
||||
if let (layout, navigationHeight) = strongSelf.controllerNode.validLayout {
|
||||
strongSelf.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
|
||||
if case .complete = result {
|
||||
dismissStatus?()
|
||||
|
||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
if let strongSelf = self, let peer {
|
||||
switch mode {
|
||||
case .fallback:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
case .custom:
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||
|
||||
let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone()
|
||||
case .suggest:
|
||||
if let navigationController = (strongSelf.navigationController as? NavigationController) {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in
|
||||
}))
|
||||
}
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))*/
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ final class TextProcessingContentComponent: Component {
|
|||
|
||||
private var currentContent: (mode: Mode, view: ComponentView<Empty>)?
|
||||
|
||||
private var currentMode: Mode = .stylize
|
||||
private var currentMode: Mode = .translate
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.currentContentBackground = UIImageView()
|
||||
|
|
@ -204,6 +204,12 @@ final class TextProcessingContentComponent: Component {
|
|||
|
||||
if self.component == nil {
|
||||
self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice
|
||||
switch component.mode {
|
||||
case .edit:
|
||||
self.currentMode = .stylize
|
||||
case .translate:
|
||||
self.currentMode = .translate
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
|
|
|||
|
|
@ -4463,7 +4463,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
let effectiveInputText: NSAttributedString
|
||||
var isEdit = false
|
||||
if effectivePresentationInterfaceState.interfaceState.editMessage != nil {
|
||||
isEdit = true
|
||||
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
|
||||
} else {
|
||||
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
|
||||
|
|
@ -4516,7 +4518,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
})
|
||||
self.sendCurrentMessage()
|
||||
},
|
||||
sendContextActions: self.chatLocation.peerId.flatMap { peerId in return TextProcessingScreen.SendContextActions(
|
||||
sendContextActions: isEdit ? nil : self.chatLocation.peerId.flatMap { peerId in return TextProcessingScreen.SendContextActions(
|
||||
peerId: peerId,
|
||||
send: { [weak self] text, mode, parameters in
|
||||
guard let self, let controller = self.controller else {
|
||||
|
|
|
|||
|
|
@ -506,6 +506,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
})
|
||||
|
||||
view.setImage(image?.withRenderingMode(.alwaysOriginal), for: [])
|
||||
view.setImage(generateTintedImage(image: image, color: interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7))?.withRenderingMode(.alwaysOriginal), for: [.highlighted])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2350,8 +2350,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
}
|
||||
}
|
||||
)
|
||||
if let createBotScreen {
|
||||
controller.push(createBotScreen)
|
||||
if let createBotScreen, let navigationController = controller.getNavigationController() {
|
||||
navigationController.pushViewController(createBotScreen)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue