Bot editing improvements

This commit is contained in:
Ilya Laktyushin 2023-04-05 16:46:48 +04:00
parent 05a3fa5bad
commit d526d47866
7 changed files with 195 additions and 31 deletions

View File

@ -9131,3 +9131,11 @@ Sorry for the inconvenience.";
"Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)"; "Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)";
"Conversation.OpenChatFolder" = "VIEW CHAT LIST"; "Conversation.OpenChatFolder" = "VIEW CHAT LIST";
"Username.BotLinkHint" = "This username cannot be edited.";
"Username.BotLinkHintExtended" = "This username cannot be edited. You can acquire additional usernames on [Fragment]().";
"PeerInfo.Bot.EditIntro" = "Edit Intro";
"PeerInfo.Bot.EditCommands" = "Edit Commands";
"PeerInfo.Bot.ChangeSettings" = "Change Bot Settings";
"PeerInfo.Bot.BotFatherInfo" = "Use [@BotFather]() to manage this bot.";

View File

@ -707,7 +707,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
var didBeginSelectingChats: (() -> Void)? var didBeginSelectingChats: (() -> Void)?
public var displayFilterLimit: (() -> Void)? public var displayFilterLimit: (() -> Void)?
public init(context: AccountContext, location: ChatListControllerLocation, chatListMode: ChatListNodeMode = .chatList, previewing: Bool, controlsHistoryPreload: Bool, isInlineMode: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) { public init(context: AccountContext, location: ChatListControllerLocation, chatListMode: ChatListNodeMode = .chatList(appendContacts: true), previewing: Bool, controlsHistoryPreload: Bool, isInlineMode: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) {
self.context = context self.context = context
self.location = location self.location = location
self.chatListMode = chatListMode self.chatListMode = chatListMode

View File

@ -20,7 +20,7 @@ import Postbox
import ChatFolderLinkPreviewScreen import ChatFolderLinkPreviewScreen
public enum ChatListNodeMode { public enum ChatListNodeMode {
case chatList case chatList(appendContacts: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool) case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool) case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool)
} }
@ -1669,6 +1669,15 @@ public final class ChatListNode: ListView {
let currentPeerId: EnginePeer.Id = context.account.peerId let currentPeerId: EnginePeer.Id = context.account.peerId
let contactList: Signal<EngineContactList?, NoError>
if case let .chatList(appendContacts) = mode, appendContacts {
contactList = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true))
|> map(Optional.init)
} else {
contactList = .single(nil)
}
let _ = contactList
let chatListNodeViewTransition = combineLatest( let chatListNodeViewTransition = combineLatest(
queue: viewProcessingQueue, queue: viewProcessingQueue,
hideArchivedFolderByDefault, hideArchivedFolderByDefault,

View File

@ -75,7 +75,8 @@ public class ItemListTextItem: ListViewItem, ItemListItem {
} }
public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
private let titleNode: TextNode private let textNode: TextNode
private var linkHighlightingNode: LinkHighlightingNode?
private let activateArea: AccessibilityAreaNode private let activateArea: AccessibilityAreaNode
private var item: ItemListTextItem? private var item: ItemListTextItem?
@ -85,17 +86,17 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
} }
public init() { public init() {
self.titleNode = TextNode() self.textNode = TextNode()
self.titleNode.isUserInteractionEnabled = false self.textNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left self.textNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale self.textNode.contentsScale = UIScreen.main.scale
self.activateArea = AccessibilityAreaNode() self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText self.activateArea.accessibilityTraits = .staticText
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode) self.addSubnode(self.textNode)
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
} }
@ -106,11 +107,16 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
recognizer.tapActionAtPoint = { _ in recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap return .waitForSingleTap
} }
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer) self.view.addGestureRecognizer(recognizer)
} }
public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.textNode)
return { item, params, neighbors in return { item, params, neighbors in
let leftInset: CGFloat = 15.0 let leftInset: CGFloat = 15.0
@ -158,7 +164,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
let _ = titleApply() let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size)
} }
}) })
} }
@ -178,9 +184,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture { switch gesture {
case .tap: case .tap:
let titleFrame = self.titleNode.frame let titleFrame = self.textNode.frame
if let item = self.item, titleFrame.contains(location) { if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url)) item.linkAction?(.tap(url))
} }
@ -194,4 +200,46 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
break break
} }
} }
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
} }

View File

@ -350,7 +350,11 @@ private func usernameSetupControllerEntries(presentationData: PresentationData,
let otherUsernames = peer.usernames.filter { !$0.flags.contains(.isEditable) } let otherUsernames = peer.usernames.filter { !$0.flags.contains(.isEditable) }
if case .bot = mode { if case .bot = mode {
entries.append(.publicLinkInfo(presentationData.theme, "This username cannot be edited.")) var infoText = presentationData.strings.Username_BotLinkHint
if otherUsernames.isEmpty {
infoText = presentationData.strings.Username_BotLinkHintExtended
}
entries.append(.publicLinkInfo(presentationData.theme, infoText))
} else { } else {
var infoText = presentationData.strings.Username_Help var infoText = presentationData.strings.Username_Help
if otherUsernames.isEmpty { if otherUsernames.isEmpty {
@ -460,21 +464,25 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup
let _ = (context.account.postbox.loadedPeerWithId(peerId) let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { peer in |> deliverOnMainQueue).start(next: { peer in
var currentAddressName: String = peer.addressName ?? "" if let user = peer as? TelegramUser, user.botInfo != nil {
updateState { state in context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
if let current = state.editingPublicLinkText { } else {
currentAddressName = current var currentAddressName: String = peer.addressName ?? ""
updateState { state in
if let current = state.editingPublicLinkText {
currentAddressName = current
}
return state
} }
return state if !currentAddressName.isEmpty {
} dismissInputImpl?()
if !currentAddressName.isEmpty { let shareController = ShareController(context: context, subject: .url("https://t.me/\(currentAddressName)"))
dismissInputImpl?() shareController.actionCompleted = {
let shareController = ShareController(context: context, subject: .url("https://t.me/\(currentAddressName)")) let presentationData = context.sharedContext.currentPresentationData.with { $0 }
shareController.actionCompleted = { presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 } }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) presentControllerImpl?(shareController, nil)
} }
presentControllerImpl?(shareController, nil)
} }
}) })
}, activateLink: { name in }, activateLink: { name in

View File

@ -5,12 +5,18 @@ import TextFormat
import Markdown import Markdown
final class PeerInfoScreenCommentItem: PeerInfoScreenItem { final class PeerInfoScreenCommentItem: PeerInfoScreenItem {
enum LinkAction {
case tap(String)
}
let id: AnyHashable let id: AnyHashable
let text: String let text: String
let linkAction: ((LinkAction) -> Void)?
init(id: AnyHashable, text: String) { init(id: AnyHashable, text: String, linkAction: ((LinkAction) -> Void)? = nil) {
self.id = id self.id = id
self.text = text self.text = text
self.linkAction = linkAction
} }
func node() -> PeerInfoScreenItemNode { func node() -> PeerInfoScreenItemNode {
@ -20,9 +26,11 @@ final class PeerInfoScreenCommentItem: PeerInfoScreenItem {
private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode { private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private var linkHighlightingNode: LinkHighlightingNode?
private let activateArea: AccessibilityAreaNode private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenCommentItem? private var item: PeerInfoScreenCommentItem?
private var presentationData: PresentationData?
override init() { override init() {
self.textNode = ImmediateTextNode() self.textNode = ImmediateTextNode()
@ -38,12 +46,28 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
} }
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenCommentItem else { guard let item = item as? PeerInfoScreenCommentItem else {
return 10.0 return 10.0
} }
self.item = item self.item = item
self.presentationData = presentationData
let sideInset: CGFloat = 16.0 + safeInsets.left let sideInset: CGFloat = 16.0 + safeInsets.left
let verticalInset: CGFloat = 7.0 let verticalInset: CGFloat = 7.0
@ -72,4 +96,69 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
return height return height
} }
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let titleFrame = self.textNode.frame
if let item = self.item, titleFrame.contains(location) {
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}
}
}
default:
break
}
}
default:
break
}
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
if let _ = self.item, let presentationData = self.presentationData {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2))
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
} }

View File

@ -1405,16 +1405,18 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
interaction.editingOpenPublicLinkSetup() interaction.editingOpenPublicLinkSetup()
})) }))
items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: "Edit Intro", icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: { items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: {
interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive))) interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive)))
})) }))
items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemCommands, text: "Edit Commands", icon: UIImage(bundleImageName: "Peer Info/BotCommands"), action: { items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemCommands, text: presentationData.strings.PeerInfo_Bot_EditCommands, icon: UIImage(bundleImageName: "Peer Info/BotCommands"), action: {
interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-commands", behavior: .interactive))) interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-commands", behavior: .interactive)))
})) }))
items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemBotSettings, text: "Change Bot Settings", icon: UIImage(bundleImageName: "Peer Info/BotSettings"), action: { items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemBotSettings, text: presentationData.strings.PeerInfo_Bot_ChangeSettings, icon: UIImage(bundleImageName: "Peer Info/BotSettings"), action: {
interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: user.addressName ?? "", behavior: .interactive))) interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: user.addressName ?? "", behavior: .interactive)))
})) }))
items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemBotInfo, text: "Use [@BotFather]() to manage this bot.")) items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemBotInfo, text: presentationData.strings.PeerInfo_Bot_BotFatherInfo, linkAction: { _ in
interaction.openPeerMention("botfather", .default)
}))
} else if !user.flags.contains(.isSupport) { } else if !user.flags.contains(.isSupport) {
let compactName = EnginePeer(user).compactDisplayTitle let compactName = EnginePeer(user).compactDisplayTitle
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: { items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: {