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()
|
super.init()
|
||||||
|
|
||||||
self.isOpaque = false
|
self.isOpaque = false
|
||||||
|
self.rasterizationScale = UIScreenScale
|
||||||
}
|
}
|
||||||
|
|
||||||
public override init(layer: Any) {
|
public override init(layer: Any) {
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
||||||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||||
return .single(.purchaseAvailable)
|
return .single(.purchaseAvailable)
|
||||||
|
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||||
|
return .single(.taken)
|
||||||
} else {
|
} else {
|
||||||
return .single(.invalid)
|
return .single(.invalid)
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +89,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
||||||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||||
return .single(.purchaseAvailable)
|
return .single(.purchaseAvailable)
|
||||||
|
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||||
|
return .single(.taken)
|
||||||
} else {
|
} else {
|
||||||
return .single(.invalid)
|
return .single(.invalid)
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +108,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
||||||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||||
return .single(.purchaseAvailable)
|
return .single(.purchaseAvailable)
|
||||||
|
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||||
|
return .single(.taken)
|
||||||
} else {
|
} else {
|
||||||
return .single(.invalid)
|
return .single(.invalid)
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +130,8 @@ func _internal_addressNameAvailability(account: Account, domain: AddressNameDoma
|
||||||
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
|> `catch` { error -> Signal<AddressNameAvailability, NoError> in
|
||||||
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
if error.errorDescription == "USERNAME_PURCHASE_AVAILABLE" {
|
||||||
return .single(.purchaseAvailable)
|
return .single(.purchaseAvailable)
|
||||||
|
} else if error.errorDescription == "USERNAME_OCCUPIED" {
|
||||||
|
return .single(.taken)
|
||||||
} else {
|
} else {
|
||||||
return .single(.invalid)
|
return .single(.invalid)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||||
private let counterTextNode: ImmediateTextNode
|
private let counterTextNode: ImmediateTextNode
|
||||||
|
|
||||||
private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)?
|
private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)?
|
||||||
|
private var heightDependentAiButtonAlpha: CGFloat = 0.0
|
||||||
|
|
||||||
public let menuButton: HighlightTrackingButtonNode
|
public let menuButton: HighlightTrackingButtonNode
|
||||||
private let menuButtonBackgroundView: GlassBackgroundView
|
private let menuButtonBackgroundView: GlassBackgroundView
|
||||||
|
|
@ -3558,7 +3559,15 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||||
transition.updateFrame(view: aiButton.button, frame: aiButtonFrame)
|
transition.updateFrame(view: aiButton.button, frame: aiButtonFrame)
|
||||||
transition.updateFrame(view: aiButton.icon, frame: image.size.centered(in: 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.button.layer, alpha: aiButtonAlpha)
|
||||||
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
transition.updateAlpha(layer: aiButton.icon.layer, alpha: aiButtonAlpha)
|
||||||
} else if let aiButton = self.aiButton {
|
} 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
|
transition.updateAlpha(layer: aiButton.icon.layer, alpha: 0.0, completion: { [weak aiButtonIconView] _ in
|
||||||
aiButtonIconView?.removeFromSuperview()
|
aiButtonIconView?.removeFromSuperview()
|
||||||
})
|
})
|
||||||
|
self.heightDependentAiButtonAlpha = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight + 64.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)
|
self.updateTextHeight(animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ final class CreateBotContentComponent: Component {
|
||||||
final class ExternalState {
|
final class ExternalState {
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
var username: String = ""
|
var username: String = ""
|
||||||
|
var usernameIsChecked: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +55,13 @@ final class CreateBotContentComponent: Component {
|
||||||
static func ==(lhs: CreateBotContentComponent, rhs: CreateBotContentComponent) -> Bool {
|
static func ==(lhs: CreateBotContentComponent, rhs: CreateBotContentComponent) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum UsernameCheckingStatus {
|
||||||
|
case checking
|
||||||
|
case valid
|
||||||
|
case invalid
|
||||||
|
case taken
|
||||||
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
private var component: CreateBotContentComponent?
|
private var component: CreateBotContentComponent?
|
||||||
|
|
@ -67,7 +75,19 @@ final class CreateBotContentComponent: Component {
|
||||||
private let usernameSection = ComponentView<Empty>()
|
private let usernameSection = ComponentView<Empty>()
|
||||||
|
|
||||||
private let usernameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
private let usernameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||||
|
private let usernameInputTag = ListMultilineTextFieldItemComponent.Tag()
|
||||||
private let nameInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
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) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -77,6 +97,11 @@ final class CreateBotContentComponent: Component {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
component.externalState.username = self.usernameInputState.text.string
|
component.externalState.username = self.usernameInputState.text.string
|
||||||
|
|
||||||
|
self.inputUsernameUpdated()
|
||||||
|
if !self.isUpdating {
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.nameInputState.updated = { [weak self] in
|
self.nameInputState.updated = { [weak self] in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
|
|
@ -89,6 +114,47 @@ final class CreateBotContentComponent: Component {
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
preconditionFailure()
|
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 {
|
func update(component: CreateBotContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
|
|
@ -210,10 +276,24 @@ final class CreateBotContentComponent: Component {
|
||||||
autocapitalizationType: .words,
|
autocapitalizationType: .words,
|
||||||
autocorrectionType: .no,
|
autocorrectionType: .no,
|
||||||
characterLimit: 64,
|
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,
|
emptyLineHandling: .notAllowed,
|
||||||
updated: { _ in },
|
updated: { _ in },
|
||||||
textUpdateTransition: .immediate
|
textUpdateTransition: .immediate,
|
||||||
|
tag: self.nameInputTag
|
||||||
)))
|
)))
|
||||||
]
|
]
|
||||||
)),
|
)),
|
||||||
|
|
@ -231,14 +311,58 @@ final class CreateBotContentComponent: Component {
|
||||||
contentHeight += nameSectionSize.height + 22.0
|
contentHeight += nameSectionSize.height + 22.0
|
||||||
|
|
||||||
var initialUsername = ""
|
var initialUsername = ""
|
||||||
|
var botSuffix = "bot"
|
||||||
if let value = component.initialUsername {
|
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)])
|
initialUsername = String(value[value.startIndex ..< value.index(value.endIndex, offsetBy: -3)])
|
||||||
} else {
|
} else {
|
||||||
initialUsername = value
|
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(
|
let usernameSectionSize = self.usernameSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
|
|
@ -252,7 +376,10 @@ final class CreateBotContentComponent: Component {
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
footer: nil,
|
footer: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(usernameFooterString),
|
||||||
|
maximumNumberOfLines: 0
|
||||||
|
)),
|
||||||
items: [
|
items: [
|
||||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||||
externalState: usernameInputState,
|
externalState: usernameInputState,
|
||||||
|
|
@ -268,11 +395,25 @@ final class CreateBotContentComponent: Component {
|
||||||
keyboardType: .asciiCapable,
|
keyboardType: .asciiCapable,
|
||||||
characterLimit: 32,
|
characterLimit: 32,
|
||||||
prefix: NSAttributedString(string: "@", font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor),
|
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),
|
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))), 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.usernameInputTag) as? ListMultilineTextFieldItemComponent.View else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemView.activateInput()
|
||||||
|
}
|
||||||
|
))),
|
||||||
|
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 0.0)
|
||||||
|
),
|
||||||
emptyLineHandling: .notAllowed,
|
emptyLineHandling: .notAllowed,
|
||||||
updated: { _ in },
|
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.name = self.nameInputState.text.string
|
||||||
component.externalState.username = self.usernameInputState.text.string
|
component.externalState.username = self.usernameInputState.text.string
|
||||||
|
component.externalState.usernameIsChecked = self.usernameCheckingStatus?.status == .valid
|
||||||
|
|
||||||
return CGSize(width: availableSize.width, height: contentHeight)
|
return CGSize(width: availableSize.width, height: contentHeight)
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +458,7 @@ private final class CreateBotSheetComponent: Component {
|
||||||
let initialUsername: String?
|
let initialUsername: String?
|
||||||
let initialTitle: String?
|
let initialTitle: String?
|
||||||
let openAutomatically: Bool
|
let openAutomatically: Bool
|
||||||
let completion: (EnginePeer.Id) -> Void
|
let completion: (EnginePeer.Id?) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
|
|
@ -324,7 +466,7 @@ private final class CreateBotSheetComponent: Component {
|
||||||
initialUsername: String?,
|
initialUsername: String?,
|
||||||
initialTitle: String?,
|
initialTitle: String?,
|
||||||
openAutomatically: Bool,
|
openAutomatically: Bool,
|
||||||
completion: @escaping (EnginePeer.Id) -> Void
|
completion: @escaping (EnginePeer.Id?) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.parentPeer = parentPeer
|
self.parentPeer = parentPeer
|
||||||
|
|
@ -349,6 +491,7 @@ private final class CreateBotSheetComponent: Component {
|
||||||
|
|
||||||
private var isCreating: Bool = false
|
private var isCreating: Bool = false
|
||||||
private var actionDisposable: Disposable?
|
private var actionDisposable: Disposable?
|
||||||
|
private var isCompleted: Bool = false
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -362,28 +505,94 @@ private final class CreateBotSheetComponent: Component {
|
||||||
self.actionDisposable?.dispose()
|
self.actionDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func validatedParams() -> (name: String, username: String)? {
|
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
|
||||||
if self.contentExternalState.name.isEmpty {
|
guard let component = self.component else {
|
||||||
return nil
|
return true
|
||||||
}
|
}
|
||||||
if self.contentExternalState.username.isEmpty {
|
if self.isCreating {
|
||||||
return nil
|
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 isUsernameValid = true
|
||||||
var usernameCharacters = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
|
var usernameCharacters = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
|
||||||
usernameCharacters.insert(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(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!)
|
||||||
usernameCharacters.insert("_")
|
usernameCharacters.insert("_")
|
||||||
for c in self.contentExternalState.username.unicodeScalars {
|
for c in inputUsername.unicodeScalars {
|
||||||
if !usernameCharacters.contains(c) {
|
if !usernameCharacters.contains(c) {
|
||||||
isUsernameValid = false
|
isUsernameValid = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isUsernameValid {
|
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 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() {
|
private func performCreateBot() {
|
||||||
|
|
@ -413,6 +622,7 @@ private final class CreateBotSheetComponent: Component {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let context = component.context
|
let context = component.context
|
||||||
|
self.isCompleted = true
|
||||||
self.animateOut.invoke(Action { [weak controller, weak navigationController] _ in
|
self.animateOut.invoke(Action { [weak controller, weak navigationController] _ in
|
||||||
if let controller, let navigationController {
|
if let controller, let navigationController {
|
||||||
controller.dismiss(completion: { [weak navigationController] in
|
controller.dismiss(completion: { [weak navigationController] in
|
||||||
|
|
@ -466,8 +676,15 @@ private final class CreateBotSheetComponent: Component {
|
||||||
let theme = environmentValue.theme
|
let theme = environmentValue.theme
|
||||||
|
|
||||||
let dismiss: (Bool) -> Void = { [weak self] animated in
|
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 {
|
if animated {
|
||||||
self?.animateOut.invoke(Action { _ in
|
self.animateOut.invoke(Action { _ in
|
||||||
if let controller = controller() {
|
if let controller = controller() {
|
||||||
controller.dismiss(completion: nil)
|
controller.dismiss(completion: nil)
|
||||||
}
|
}
|
||||||
|
|
@ -544,7 +761,17 @@ private final class CreateBotSheetComponent: Component {
|
||||||
isCentered: environmentValue.metrics.widthClass == .regular,
|
isCentered: environmentValue.metrics.widthClass == .regular,
|
||||||
screenSize: availableSize,
|
screenSize: availableSize,
|
||||||
regularMetricsSize: nil,
|
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)
|
dismiss(animated)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -581,7 +808,7 @@ public class CreateBotScreen: ViewControllerComponentContainer {
|
||||||
initialUsername: String?,
|
initialUsername: String?,
|
||||||
initialTitle: String?,
|
initialTitle: String?,
|
||||||
openAutomatically: Bool,
|
openAutomatically: Bool,
|
||||||
completion: @escaping (EnginePeer.Id) -> Void
|
completion: @escaping (EnginePeer.Id?) -> Void
|
||||||
) async {
|
) async {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
|
|
@ -609,6 +836,14 @@ public class CreateBotScreen: ViewControllerComponentContainer {
|
||||||
self.statusBar.statusBarStyle = .Ignore
|
self.statusBar.statusBarStyle = .Ignore
|
||||||
self.navigationPresentation = .flatModal
|
self.navigationPresentation = .flatModal
|
||||||
self.blocksBackgroundWhenInOverlay = true
|
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) {
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
|
@ -777,13 +1012,16 @@ private final class ActionButtonsComponent: Component {
|
||||||
private final class EditLabelComponent: Component {
|
private final class EditLabelComponent: Component {
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
strings: PresentationStrings
|
strings: PresentationStrings,
|
||||||
|
action: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: EditLabelComponent, rhs: EditLabelComponent) -> Bool {
|
static func ==(lhs: EditLabelComponent, rhs: EditLabelComponent) -> Bool {
|
||||||
|
|
@ -805,12 +1043,23 @@ private final class EditLabelComponent: Component {
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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 {
|
func update(component: EditLabelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
@ -846,6 +1095,7 @@ private final class EditLabelComponent: Component {
|
||||||
)
|
)
|
||||||
if let backgroundView = self.background.view {
|
if let backgroundView = self.background.view {
|
||||||
if backgroundView.superview == nil {
|
if backgroundView.superview == nil {
|
||||||
|
backgroundView.isUserInteractionEnabled = false
|
||||||
self.addSubview(backgroundView)
|
self.addSubview(backgroundView)
|
||||||
}
|
}
|
||||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||||
|
|
@ -853,6 +1103,7 @@ private final class EditLabelComponent: Component {
|
||||||
|
|
||||||
if let titleView = self.title.view {
|
if let titleView = self.title.view {
|
||||||
if titleView.superview == nil {
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
self.addSubview(titleView)
|
self.addSubview(titleView)
|
||||||
}
|
}
|
||||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
||||||
case legacy
|
case legacy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class Tag {
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class ExternalState {
|
public final class ExternalState {
|
||||||
public fileprivate(set) var hasText: Bool = false
|
public fileprivate(set) var hasText: Bool = false
|
||||||
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,11 @@ public extension PeerInfoScreenImpl {
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
controller.videoCompletion = { [weak parentController] image, url, values, markup, commit in
|
controller.videoCompletion = { [weak parentController] image, url, values, markup, commit in
|
||||||
|
guard let parentController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
resultImage = image
|
resultImage = image
|
||||||
let _ = parentController
|
updateProfileVideo(parentController: parentController, context: context, peer: peer, image: image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
|
||||||
updateProfileVideo(context: context, image: image, video: nil, values: nil, markup: markup, mode: mode, uploadStatus: uploadStatusPromise)
|
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
parentController.push(controller)
|
parentController.push(controller)
|
||||||
|
|
@ -193,7 +195,7 @@ public extension PeerInfoScreenImpl {
|
||||||
case let .video(video, coverImage, values, _, _):
|
case let .video(video, coverImage, values, _, _):
|
||||||
if let coverImage {
|
if let coverImage {
|
||||||
resultImage = 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({})
|
commit({})
|
||||||
default:
|
default:
|
||||||
|
|
@ -283,52 +285,24 @@ public extension PeerInfoScreenImpl {
|
||||||
|
|
||||||
if case .complete = result {
|
if case .complete = result {
|
||||||
dismissStatus?()
|
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?) {
|
static func updateProfileVideo(parentController: ViewController, context: AccountContext, peer: EnginePeer, 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)
|
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>?) {
|
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
|
var uploadVideo = true
|
||||||
if let _ = markup {
|
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
|
uploadVideo = true
|
||||||
} else {
|
} else {
|
||||||
uploadVideo = false
|
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))
|
uploadStatus?.set(.single(.done))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -338,8 +312,8 @@ public extension PeerInfoScreenImpl {
|
||||||
videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0)
|
videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let account = self.context.account
|
let account = context.account
|
||||||
let context = self.context
|
let context = context
|
||||||
|
|
||||||
let videoResource: Signal<TelegramMediaResource?, UploadPeerPhotoError>
|
let videoResource: Signal<TelegramMediaResource?, UploadPeerPhotoError>
|
||||||
if uploadVideo, let video, let values {
|
if uploadVideo, let video, let values {
|
||||||
|
|
@ -388,10 +362,7 @@ public extension PeerInfoScreenImpl {
|
||||||
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
|
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 = MediaEditorVideoExport(postbox: context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: tempFile.path, textScale: 2.0)
|
||||||
let _ = (videoExport.status
|
let _ = (videoExport.status
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] status in
|
|> deliverOnMainQueue).startStandalone(next: { status in
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch status {
|
switch status {
|
||||||
case .completed:
|
case .completed:
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) {
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) {
|
||||||
|
|
@ -401,11 +372,8 @@ public extension PeerInfoScreenImpl {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
EngineTempBox.shared.dispose(tempFile)
|
EngineTempBox.shared.dispose(tempFile)
|
||||||
case let .progress(progress):
|
case .progress:
|
||||||
Queue.mainQueue().async {
|
break
|
||||||
self.controllerNode.state = self.controllerNode.state.withAvatarUploadProgress(.value(CGFloat(progress * 0.45)))
|
|
||||||
self.requestLayout(transition: .immediate)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -422,100 +390,41 @@ public extension PeerInfoScreenImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
var dismissStatus: (() -> Void)?
|
var dismissStatus: (() -> Void)?
|
||||||
if [.suggest, .fallback, .accept].contains(mode) {
|
if "".isEmpty {
|
||||||
let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
self?.controllerNode.updateAvatarDisposable.set(nil)
|
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
dismissStatus?()
|
dismissStatus?()
|
||||||
}))
|
}))
|
||||||
dismissStatus = { [weak statusController] in
|
dismissStatus = { [weak statusController] in
|
||||||
statusController?.dismiss()
|
statusController?.dismiss()
|
||||||
}
|
}
|
||||||
if let topController = self.navigationController?.topViewController as? ViewController {
|
if let topController = parentController.navigationController?.topViewController as? ViewController {
|
||||||
topController.presentInGlobalOverlay(statusController)
|
|
||||||
} else if let topController = self.parentController?.topViewController as? ViewController {
|
|
||||||
topController.presentInGlobalOverlay(statusController)
|
topController.presentInGlobalOverlay(statusController)
|
||||||
} else {
|
} else {
|
||||||
self.presentInGlobalOverlay(statusController)
|
parentController.presentInGlobalOverlay(statusController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let peerId = self.peerId
|
let peerId = peer.id
|
||||||
let isSettings = self.isSettings
|
let updateAvatarDisposable = MetaDisposable()
|
||||||
let isMyProfile = self.isMyProfile
|
updateAvatarDisposable.set((videoResource
|
||||||
self.controllerNode.updateAvatarDisposable.set((videoResource
|
|
||||||
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
|
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
|
||||||
if isSettings || isMyProfile {
|
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
|
||||||
if case .fallback = mode {
|
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch result {
|
switch result {
|
||||||
case .complete:
|
case .complete:
|
||||||
uploadStatus?.set(.single(.done))
|
uploadStatus?.set(.single(.done))
|
||||||
strongSelf.controllerNode.state = strongSelf.controllerNode.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil)
|
|
||||||
case let .progress(value):
|
case let .progress(value):
|
||||||
uploadStatus?.set(.single(.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 {
|
if case .complete = result {
|
||||||
dismissStatus?()
|
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 currentContent: (mode: Mode, view: ComponentView<Empty>)?
|
||||||
|
|
||||||
private var currentMode: Mode = .stylize
|
private var currentMode: Mode = .translate
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.currentContentBackground = UIImageView()
|
self.currentContentBackground = UIImageView()
|
||||||
|
|
@ -204,6 +204,12 @@ final class TextProcessingContentComponent: Component {
|
||||||
|
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice
|
self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice
|
||||||
|
switch component.mode {
|
||||||
|
case .edit:
|
||||||
|
self.currentMode = .stylize
|
||||||
|
case .translate:
|
||||||
|
self.currentMode = .translate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.component = component
|
self.component = component
|
||||||
|
|
|
||||||
|
|
@ -4463,7 +4463,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
let effectiveInputText: NSAttributedString
|
let effectiveInputText: NSAttributedString
|
||||||
|
var isEdit = false
|
||||||
if effectivePresentationInterfaceState.interfaceState.editMessage != nil {
|
if effectivePresentationInterfaceState.interfaceState.editMessage != nil {
|
||||||
|
isEdit = true
|
||||||
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
|
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.effectiveInputState.inputText)
|
||||||
} else {
|
} else {
|
||||||
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
|
effectiveInputText = expandedInputStateAttributedString(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
|
||||||
|
|
@ -4516,7 +4518,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||||
})
|
})
|
||||||
self.sendCurrentMessage()
|
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,
|
peerId: peerId,
|
||||||
send: { [weak self] text, mode, parameters in
|
send: { [weak self] text, mode, parameters in
|
||||||
guard let self, let controller = self.controller else {
|
guard let self, let controller = self.controller else {
|
||||||
|
|
|
||||||
|
|
@ -506,6 +506,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||||
})
|
})
|
||||||
|
|
||||||
view.setImage(image?.withRenderingMode(.alwaysOriginal), for: [])
|
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 {
|
if let createBotScreen, let navigationController = controller.getNavigationController() {
|
||||||
controller.push(createBotScreen)
|
navigationController.pushViewController(createBotScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue