Various improvements

This commit is contained in:
Isaac 2026-03-24 16:47:36 +08:00
parent 9c0e6f6151
commit a0a2f9f6bf
10 changed files with 348 additions and 145 deletions

View file

@ -319,6 +319,7 @@ public class CheckLayer: CALayer {
super.init()
self.isOpaque = false
self.rasterizationScale = UIScreenScale
}
public override init(layer: Any) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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