Various improvements

This commit is contained in:
isaac 2026-04-28 00:44:54 +04:00
parent 1b85d8ccb0
commit 55b2b47797
8 changed files with 165 additions and 26 deletions

View file

@ -1308,7 +1308,7 @@ public final class TextProcessingScreenSendContextActions {
public enum TextProcessingScreenMode {
case edit(saveRestoreStateId: EnginePeer.Id?, completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?, sendContextActions: TextProcessingScreenSendContextActions?)
case translate(fromLanguage: String?)
case preview(style: TelegramComposeAIMessageMode.CloudStyle.Custom, authorPeer: EnginePeer?, initialPreview: AIMessageStylePreview?, added: () -> Void)
case preview(style: TelegramComposeAIMessageMode.CloudStyle.Custom, authorPeer: EnginePeer?, initialPreview: AIMessageStylePreview?, isAlreadyAdded: Bool, added: () -> Void)
}
public enum EmojiStatusSelectionControllerMode {

View file

@ -173,6 +173,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
initialFilter = .topics
}
var folder = folder
folder = nil
self.context = context
self.peersFilter = filter
self.requestPeerType = requestPeerType

View file

@ -661,6 +661,7 @@ public final class ResizableSheetComponent<ChildEnvironmentType: Sendable & Equa
guard let self else {
return
}
self.endEditing(true)
self.animateOut {
completion(Void())
}

View file

@ -902,7 +902,9 @@ func _internal_editAITextStyle(account: Account, id: Int64, accessHash: Int64, d
let styles = _internal_cachedCloudAITextStyles(transaction: transaction)
var items = styles?.items ?? []
items.append(style)
if let index = items.firstIndex(where: { $0.id == .style(.custom(id)) }) {
items[index] = style
}
_internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles(items: items, hash: 0))
return style
}

View file

@ -356,7 +356,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
contentMediaValue = file
} else if file.isVideo {
contentMediaValue = file
} else if file.isSticker || file.isAnimatedSticker {
} else if file.isSticker || file.isAnimatedSticker || file.isCustomEmoji {
contentMediaValue = file
} else {
contentFileValue = file

View file

@ -54,6 +54,7 @@ final class TextProcessingContentComponent: Component {
let newStyleAdded: (TelegramComposeAIMessageMode.CloudStyle) -> Void
let styleUpdated: (TelegramComposeAIMessageMode.CloudStyle) -> Void
let styleDeleted: (TelegramComposeAIMessageMode.StyleId) -> Void
let displayToast: (String) -> Void
let dismiss: (@escaping () -> Void) -> Void
init(
@ -72,6 +73,7 @@ final class TextProcessingContentComponent: Component {
newStyleAdded: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void,
styleUpdated: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void,
styleDeleted: @escaping (TelegramComposeAIMessageMode.StyleId) -> Void,
displayToast: @escaping (String) -> Void,
dismiss: @escaping (@escaping () -> Void) -> Void
) {
self.externalState = externalState
@ -89,6 +91,7 @@ final class TextProcessingContentComponent: Component {
self.newStyleAdded = newStyleAdded
self.styleUpdated = styleUpdated
self.styleDeleted = styleDeleted
self.displayToast = displayToast
self.dismiss = dismiss
}
@ -240,7 +243,26 @@ final class TextProcessingContentComponent: Component {
guard let slug = style.slug else {
return
}
let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .url("https://t.me/addstyle/\(slug)")))
let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(
subject: .url("https://t.me/addstyle/\(slug)"),
completed: { [weak self] peerIds in
Task { @MainActor in
guard let self, let component = self.component, let environment = self.environment, let peerId = peerIds.first else {
return
}
let text: String
if peerId == component.context.account.peerId {
text = environment.strings.WebBrowser_LinkForwardTooltip_SavedMessages_One
} else {
guard let peer = await component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)).get() else {
return
}
text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
}
component.displayToast(text)
}
}
))
self.environment?.controller()?.present(shareController, in: .window(.root))
}
@ -411,7 +433,7 @@ final class TextProcessingContentComponent: Component {
guard let component = self.component else {
return
}
guard case let .preview(style, _, _, _) = component.mode else {
guard case let .preview(style, _, _, _, _) = component.mode else {
return
}
@ -463,7 +485,7 @@ final class TextProcessingContentComponent: Component {
switch component.mode {
case .edit:
self.currentMode = .stylize
case let .preview(_, _, initialPreview, _):
case let .preview(_, _, initialPreview, _, _):
self.currentMode = .stylize
self.currentStylePreview = initialPreview
self.requestStylePreview()
@ -476,7 +498,7 @@ final class TextProcessingContentComponent: Component {
var contentHeight: CGFloat = 0.0
if case let .preview(style, _, _, _) = component.mode {
if case let .preview(style, _, _, _, _) = component.mode {
contentHeight += 40.0
if let previewIconFile = component.previewIconFile {
let previewIcon: ComponentView<Empty>
@ -736,7 +758,7 @@ final class TextProcessingContentComponent: Component {
var isRequestingPreview: Bool = false
var authorPeer: EnginePeer?
var userCount: Int = 0
if case let .preview(style, authorPeerValue, _, _) = component.mode {
if case let .preview(style, authorPeerValue, _, _, _) = component.mode {
isPreview = true
inputText = TextWithEntities(text: "", entities: [])
authorPeer = authorPeerValue
@ -1191,6 +1213,9 @@ private final class TextProcessingSheetComponent: Component {
private var styleCreatedToastData: (timer: Foundation.Timer, emojiFile: TelegramMediaFile, style: TelegramComposeAIMessageMode.CloudStyle.Custom)?
private var styleCreatedToast: ComponentView<Empty>?
private var customToastData: (timer: Foundation.Timer, text: String)?
private var customToast: ComponentView<Empty>?
private var component: TextProcessingSheetComponent?
private var environment: ViewControllerComponentContainer.Environment?
@ -1211,6 +1236,7 @@ private final class TextProcessingSheetComponent: Component {
deinit {
self.actionDisposable?.dispose()
self.styleCreatedToastData?.timer.invalidate()
self.customToastData?.timer.invalidate()
}
private func displayLongPressSendMenu(sourceSendButton: UIView) {
@ -1410,8 +1436,9 @@ private final class TextProcessingSheetComponent: Component {
performMainAction = {
dismiss(true)
}
case let .preview(style, _, _, added):
actionButtonTitle = "Add Style"
case let .preview(style, _, _, isAlreadyAdded, added):
//TODO:localize
actionButtonTitle = isAlreadyAdded ? "Done" : "Add Style"
isMainActionEnabled = !self.isPerformingMainAction
performMainAction = { [weak self] in
guard let self, let component = self.component else {
@ -1422,19 +1449,23 @@ private final class TextProcessingSheetComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4))
}
self.actionDisposable?.dispose()
self.actionDisposable = (component.context.engine.messages.installAIMessageStyle(style: style) |> deliverOnMainQueue).startStrict(error: { [weak self] _ in
guard let self else {
return
}
self.isPerformingMainAction = false
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}, completed: {
if isAlreadyAdded {
dismiss(true)
added()
})
} else {
self.actionDisposable?.dispose()
self.actionDisposable = (component.context.engine.messages.installAIMessageStyle(style: style) |> deliverOnMainQueue).startStrict(error: { [weak self] _ in
guard let self else {
return
}
self.isPerformingMainAction = false
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}, completed: {
dismiss(true)
added()
})
}
}
}
}
@ -1549,6 +1580,19 @@ private final class TextProcessingSheetComponent: Component {
self.styles.remove(at: index)
self.state?.updated(transition: .spring(duration: 0.4))
},
displayToast: { [weak self] text in
guard let self else {
return
}
self.customToastData = (Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.customToastData = nil
self.state?.updated(transition: .spring(duration: 0.4))
}), text)
self.state?.updated(transition: .spring(duration: 0.4))
},
dismiss: { [weak self] completion in
self?.animateOut.invoke(Action { _ in
if let controller = controller() {
@ -1787,6 +1831,63 @@ private final class TextProcessingSheetComponent: Component {
}
}
}
if let customToastData = self.customToastData, !self.contentExternalState.nonPremiumFloodTriggered, self.styleCreatedToastData == nil {
let sideInset: CGFloat = 8.0
let customToast: ComponentView<Empty>
var customToastTransition = transition
if let current = self.customToast {
customToast = current
} else {
customToastTransition = customToastTransition.withAnimation(.none)
customToast = ComponentView()
self.customToast = customToast
}
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let playOnce = ActionSlot<Void>()
let customToastSize = customToast.update(
transition: customToastTransition,
component: AnyComponent(ToastContentComponent(
icon: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "anim_infotip"),
startingPosition: .begin,
size: CGSize(width: 32.0, height: 32.0),
playOnce: playOnce
)),
content: AnyComponent(VStack([
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .markdown(text: customToastData.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })),
maximumNumberOfLines: 0
)))
], alignment: .left, spacing: 6.0)),
insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0),
iconSpacing: 12.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
if let customToastView = customToast.view {
if customToastView.superview == nil, let sheetView = self.sheet.view as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
sheetView.containerView.addSubview(customToastView)
if !transition.animation.isImmediate {
customToastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
playOnce.invoke(())
}
customToastTransition.setFrame(view: customToastView, frame: CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - 94.0 - customToastSize.height), size: customToastSize))
}
} else {
if let customToast = self.customToast {
self.customToast = nil
if let customToastView = customToast.view {
customToastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak customToastView] _ in
customToastView?.removeFromSuperview()
})
}
}
}
if let languageSelectionMenuDataValue = self.languageSelectionMenuData {
let languageSelectionMenu: ComponentView<Empty>
@ -1988,7 +2089,7 @@ public class TextProcessingScreen: ViewControllerComponentContainer {
var shouldDisplayStyleNotice = false
var previewIconFile: TelegramMediaFile?
if case let .preview(style, _, _, _) = mode {
if case let .preview(style, _, _, _, _) = mode {
if let emojiFileId = style.emojiFileId {
previewIconFile = await context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get().first?.value
}

View file

@ -67,6 +67,7 @@ final class TextStyleEditContentComponent: Component {
private var environment: ViewControllerComponentContainer.Environment?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var previousEnvironment: ViewControllerComponentContainer.Environment?
private let iconBackground = ComponentView<Empty>()
private let emptyIcon = ComponentView<Empty>()
@ -209,6 +210,8 @@ final class TextStyleEditContentComponent: Component {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let previousEnvironment = self.previousEnvironment
self.previousEnvironment = environment
var resetTitle: String?
var resetText: String?
@ -534,8 +537,36 @@ final class TextStyleEditContentComponent: Component {
let _ = alphaTransition
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textChanged = hint.kind, let hintView = hint.view {
self.recenterCaret(hintView: hintView, transition: transition)
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let hintView = hint.view {
switch hint.kind {
case .textChanged:
self.recenterCaret(hintView: hintView, transition: transition)
default:
break
}
}
if let previousEnvironment {
var targetView: UIView?
if component.externalState.titleInputState.isEditing {
if let view = self.titleSection.findTaggedView(tag: self.titleFieldTag) as? ListMultilineTextFieldItemComponent.View {
targetView = view
}
} else if component.externalState.textInputState.isEditing {
if let view = self.textSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
targetView = view
}
}
if let targetView {
if (environment.inputHeight == 0.0) != (previousEnvironment.inputHeight == 0.0) {
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.recenterCaret(hintView: targetView, transition: transition)
}
}
}
}
return CGSize(width: availableSize.width, height: contentHeight)

View file

@ -2046,8 +2046,9 @@ func openResolvedUrlImpl(
TelegramEngine.EngineData.Item.Peer.Peer(id: authorId)
).get()
}
let isAlreadyAdded = await context.engine.messages.composeAIMessageStyles().get().contains(where: { $0.id == .style(.custom(style.id)) })
let controller = await context.sharedContext.makeTextProcessingScreen(context: context, theme: nil, mode: .preview(style: style, authorPeer: authorPeer, initialPreview: initialPreview, added: {
let controller = await context.sharedContext.makeTextProcessingScreen(context: context, theme: nil, mode: .preview(style: style, authorPeer: authorPeer, initialPreview: initialPreview, isAlreadyAdded: isAlreadyAdded, added: {
Task { @MainActor in
guard let emojiFileId = style.emojiFileId, let file = await context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]).get().first?.value else {
return