Various improvements

This commit is contained in:
Ilya Laktyushin 2026-04-29 20:24:29 +02:00
parent 06a0031bdd
commit 14bbdca050
11 changed files with 172 additions and 19 deletions

View file

@ -16256,3 +16256,11 @@ Error: %8$@";
"Settings.ChatAutomation" = "Chat Automation";
"Settings.ChatAutomationInfo" = "Add a bot to reply to messages on your behalf.";
"Settings.ChatAutomationOff" = "Off";
"Chat.SendReactionRestricted" = "You cannot send reactions in this chat.";
"ChatbotSetup.BotInstalled" = "%@ now manages your account.";
"ChatbotSetup.SetupNotCompleted.Title" = "No Bot Added";
"ChatbotSetup.SetupNotCompleted.Text" = "You havent added a bot to manage your account. Leave anyway?";
"ChatbotSetup.SetupNotCompleted.Leave" = "Leave";

View file

@ -20,6 +20,7 @@ swift_library(
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/ContextUI:ContextUI",
"//submodules/AvatarNode:AvatarNode",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",

View file

@ -2,6 +2,7 @@ import Foundation
import AsyncDisplayKit
import Display
import ComponentFlow
import MultilineTextComponent
import SwiftSignalKit
import TelegramCore
import AccountContext
@ -373,12 +374,13 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
let availableReactions: AvailableReactions?
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
private let highlightBackgroundView: UIView
let avatarNode: AvatarNode
let titleLabelNode: ImmediateTextNode
let textLabelNode: ImmediateTextNode
let readIconView: UIImageView
var credibilityIconView: ComponentView<Empty>?
private var reactionLayer: InlineStickerItemLayer?
private var iconFrame: CGRect?
private var file: TelegramMediaFile?
@ -419,16 +421,35 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.readIconView = UIImageView(image: readIconImage)
self.highlightBackgroundView = UIView()
self.highlightBackgroundView.alpha = 0.0
self.highlightBackgroundView.isUserInteractionEnabled = false
super.init()
self.isAccessibilityElement = true
self.view.insertSubview(self.highlightBackgroundView, at: 0)
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleLabelNode)
self.addSubnode(self.textLabelNode)
self.view.addSubview(self.readIconView)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.highlightBackgroundView.layer.removeAnimation(forKey: "opacity")
self.highlightBackgroundView.alpha = 0.1
} else {
let currentAlpha = self.highlightBackgroundView.alpha
self.highlightBackgroundView.alpha = 0.0
self.highlightBackgroundView.layer.animateAlpha(from: currentAlpha, to: 0.0, duration: 0.3)
}
}
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:)))
longTapRecognizer.isEnabled = false
@ -537,7 +558,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
let sideInset: CGFloat = 16.0
let reaction: MessageReaction.Reaction? = item.reaction
self.highlightBackgroundView.backgroundColor = presentationData.theme.overallDarkAppearance ? UIColor.white : UIColor.black
self.highlightBackgroundView.setMonochromaticEffect(tintColor: self.highlightBackgroundView.backgroundColor)
self.highlightBackgroundView.frame = CGRect(origin: CGPoint(x: 10.0, y: 5.0), size: CGSize(width: max(0.0, size.width - 20.0), height: size.height - 10.0))
self.highlightBackgroundView.layer.cornerRadius = self.highlightBackgroundView.frame.height * 0.5
if self.displayReactionIcon, reaction != self.item?.reaction {
if let reaction = reaction {
switch reaction {
@ -810,6 +836,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private var hasMore: Bool = false
private let scrollNode: ASScrollNode
private let separatorNode: ASDisplayNode
private var ignoreScrolling: Bool = false
private var animateIn: Bool = false
private var bottomScrollInset: CGFloat = 0.0
@ -826,7 +853,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private var placeholderItemImage: UIImage?
private var placeholderLayers: [Int: SimpleLayer] = [:]
private let deleteReactionInfoText: ComponentView<Empty>
init(
context: AccountContext,
displayReadTimestamps: Bool,
@ -858,6 +887,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, readStats: readStats, reaction: reaction), readStats: readStats)
self.scrollNode = ASScrollNode()
self.separatorNode = ASDisplayNode()
self.deleteReactionInfoText = ComponentView<Empty>()
self.scrollNode.canCancelAllTouchesInViews = true
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.showsVerticalScrollIndicator = false
@ -871,6 +903,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.addSubnode(self.scrollNode)
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.separatorNode.isHidden = true
self.separatorNode.isUserInteractionEnabled = false
self.scrollNode.addSubnode(self.separatorNode)
self.clipsToBounds = true
self.stateDisposable = (self.listContext.state
@ -965,7 +1001,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
}
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad)
if let deleteReaction = self.deleteReaction, let reaction = item.reaction {
if let deleteReaction = self.deleteReaction, let reaction = item.reaction, item.peer.id != self.context.account.peerId {
let peer = item.peer
itemNode.longTapAction = {
deleteReaction(peer, reaction)
@ -1079,7 +1115,70 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
}
self.presentationData = presentationData
let size = CGSize(width: constrainedSize.width, height: topInset + CGFloat(self.state.totalCount) * itemHeight + topInset)
let baseContentHeight = topInset + CGFloat(self.state.totalCount) * itemHeight + topInset
var footerHeight: CGFloat = 0.0
let displayDeleteReactionInfoFooter: Bool
if self.deleteReaction != nil {
displayDeleteReactionInfoFooter = self.state.mergedItems.contains(where: { item in
return item.reaction != nil && item.peer.id != self.context.account.peerId
})
} else {
displayDeleteReactionInfoFooter = false
}
if displayDeleteReactionInfoFooter {
let separatorHeight: CGFloat = 20.0
let horizontalInset: CGFloat = 18.0
let textTopInset: CGFloat = 5.0
let textBottomInset: CGFloat = 18.0
let textFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0))
self.separatorNode.isHidden = false
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
var animateIn = false
var footerTransition = transition
if self.deleteReactionInfoText.view?.superview == nil, footerTransition.isAnimated {
footerTransition = .immediate
animateIn = true
}
footerTransition.updateFrame(node: self.separatorNode, frame: CGRect(
origin: CGPoint(x: horizontalInset, y: baseContentHeight + floorToScreenPixels((separatorHeight - UIScreenPixel) * 0.5)),
size: CGSize(width: max(0.0, constrainedSize.width - horizontalInset * 2.0), height: 1.0)
))
let textSize = self.deleteReactionInfoText.update(
transition: ComponentTransition(footerTransition),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationData.strings.Chat_DeleteReactionInfo,
font: textFont,
textColor: presentationData.theme.contextMenu.primaryColor
)),
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: max(0.0, constrainedSize.width - horizontalInset * 2.0), height: .greatestFiniteMagnitude)
)
if let textView = self.deleteReactionInfoText.view {
if textView.superview == nil {
self.scrollNode.view.addSubview(textView)
textView.isUserInteractionEnabled = false
}
footerTransition.updateFrame(view: textView, frame: CGRect(origin: CGPoint(x: horizontalInset, y: baseContentHeight + separatorHeight + textTopInset), size: textSize))
if animateIn {
self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
footerHeight = separatorHeight + textTopInset + textSize.height + textBottomInset
} else {
self.separatorNode.isHidden = true
self.deleteReactionInfoText.view?.removeFromSuperview()
}
let size = CGSize(width: constrainedSize.width, height: baseContentHeight + footerHeight)
let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height))
self.currentSize = containerSize

View file

@ -36,6 +36,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/ShimmerEffect",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",

View file

@ -208,7 +208,11 @@ final class ChatbotSearchResultItemComponent: Component {
case let .found(peer, _):
isTextVisible = true
titleValue = peer.displayTitle(strings: component.strings, displayOrder: .firstLast)
subtitleValue = component.strings.Bot_GenericBotStatus
if let addressName = peer.addressName {
subtitleValue = "@\(addressName)"
} else {
subtitleValue = component.strings.Bot_GenericBotStatus
}
}
let titleSize = self.titleLabel.update(

View file

@ -25,6 +25,7 @@ import Markdown
import PeerListItemComponent
import AvatarNode
import AlertComponent
import UndoUI
private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
@ -216,10 +217,21 @@ final class ChatbotSetupScreenComponent: Component {
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
guard let component = self.component else {
guard let component = self.component, let environemnt = self.environment else {
return true
}
if let botResolutionState = self.botResolutionState, case let .found(_, isInstalled) = botResolutionState.state, !isInstalled {
let alertController = textAlertController(context: component.context, title: environemnt.strings.ChatbotSetup_SetupNotCompleted_Title, text: environemnt.strings.ChatbotSetup_SetupNotCompleted_Text, actions: [
TextAlertAction(type: .defaultAction, title: environemnt.strings.Common_Cancel, action: {}),
TextAlertAction(type: .genericAction, title: environemnt.strings.ChatbotSetup_SetupNotCompleted_Leave, action: {
complete()
})
])
environemnt.controller()?.present(alertController, in: .window(.root))
return false
}
var mappedCategories: TelegramBusinessRecipients.Categories = []
if self.additionalPeerList.categories.contains(.existingChats) {
mappedCategories.insert(.existingChats)
@ -803,7 +815,7 @@ final class ChatbotSetupScreenComponent: Component {
strings: environment.strings,
content: mappedContent,
installAction: { [weak self] in
guard let self else {
guard let self, let component = self.component, let environment = self.environment, let controller = self.environment?.controller() else {
return
}
self.endEditing(true)
@ -820,6 +832,14 @@ final class ChatbotSetupScreenComponent: Component {
})
]), in: .window(.root))
}
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, action: nil, duration: 2.0),
elevatedLayout: false,
position: .bottom,
animateInAsReplacement: false,
action: { _ in return true }
), in: .current)
}
},
removeAction: { [weak self] in

View file

@ -374,7 +374,13 @@ extension ChatControllerImpl {
if case .stars = chosenUpdatedReaction.reaction {
if !canSendReactionsToChat(self.presentationInterfaceState) {
controller?.dismiss(completion: {})
if let controller {
controller.dismiss(completion: { [weak self] in
self?.displaySendReactionRestrictedToast()
})
} else {
self.displaySendReactionRestrictedToast()
}
return
}
@ -514,7 +520,13 @@ extension ChatControllerImpl {
}
if removedReaction == nil && !canSendReactionsToChat(self.presentationInterfaceState) {
controller?.dismiss(completion: {})
if let controller {
controller.dismiss(completion: { [weak self] in
self?.displaySendReactionRestrictedToast()
})
} else {
self.displaySendReactionRestrictedToast()
}
return
}

View file

@ -10,6 +10,17 @@ import AccountContext
import ChatControllerInteraction
extension ChatControllerImpl {
func displaySendReactionRestrictedToast() {
self.controllerInteraction?.presentControllerInCurrent(UndoOverlayController(
presentationData: self.presentationData,
content: .banned(text: self.presentationData.strings.Chat_SendReactionRestricted),
elevatedLayout: false,
position: .bottom,
animateInAsReplacement: false,
action: { _ in return true }
), nil)
}
func displayPostedScheduledMessagesToast(ids: [EngineMessage.Id]) {
let timestamp = CFAbsoluteTimeGetCurrent()
if self.lastPostedScheduledMessagesToastTimestamp + 0.4 >= timestamp {

View file

@ -1682,9 +1682,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case .default = reaction, strongSelf.chatLocation.peerId == strongSelf.context.account.peerId {
return
}
if case .default = reaction, !canSendReactionsToChat(strongSelf.presentationInterfaceState) {
return
}
if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject {
if case let .hashTagSearch(publicPosts) = customChatContents.kind, publicPosts {
return
@ -1798,6 +1795,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case .stars = chosenReaction {
if !canSendReactionsToChat(strongSelf.presentationInterfaceState) {
strongSelf.displaySendReactionRestrictedToast()
return
}
@ -1968,6 +1966,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if removedReaction == nil {
if !canSendReactionsToChat(strongSelf.presentationInterfaceState) {
strongSelf.displaySendReactionRestrictedToast()
return
}

View file

@ -263,7 +263,7 @@ extension ChatControllerImpl {
}
public func presentReactionDeletionOptions(author: Peer, messageId: MessageId) {
guard self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel else {
guard self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel, author.id != self.context.account.peerId else {
return
}
let _ = (self.context.sharedContext.chatAvailableMessageActions(

View file

@ -321,9 +321,7 @@ extension ChatControllerImpl {
let presentationContext = self.controllerInteraction?.presentationContext
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
if canDeleteReactions && canViewReactions {
items.tip = .deleteReaction
} else if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
items.tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil)
if packReferences.count > 1 {