diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0be9557f87..b4b193d085 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12050,3 +12050,5 @@ Sorry for the inconvenience."; "MediaEditor.NewStickerPack.Title" = "New Sticker Set"; "MediaEditor.NewStickerPack.Text" = "Choose a name for your sticker set."; +"Premium.Gift.ContactSelection.SendMessage" = "Send Message"; +"Premium.Gift.ContactSelection.OpenProfile" = "Open Profile"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 43768a9bc6..b779e3322b 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -637,6 +637,7 @@ public enum ContactListAction: Equatable { case generic case voiceCall case videoCall + case more } public enum ContactListPeer: Equatable { diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 4d00d76eb1..5de19b6719 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -102,8 +102,10 @@ public final class ContactMultiselectionControllerParams { public let alwaysEnabled: Bool public let limit: Int32? public let reachedLimit: ((Int32) -> Void)? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil) { + public let openProfile: ((EnginePeer) -> Void)? + public let sendMessage: ((EnginePeer) -> Void)? + + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { self.context = context self.updatedPresentationData = updatedPresentationData self.mode = mode @@ -116,6 +118,8 @@ public final class ContactMultiselectionControllerParams { self.alwaysEnabled = alwaysEnabled self.limit = limit self.reachedLimit = reachedLimit + self.openProfile = openProfile + self.sendMessage = sendMessage } } diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index 1ce3cafb91..ef6c5d49a2 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -45,6 +45,7 @@ swift_library( "//submodules/TooltipUI", "//submodules/UndoUI", "//submodules/TelegramIntents", + "//submodules/ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 004f5acb75..fbfffde7a1 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -23,6 +23,7 @@ import AppBundle import ContextUI import PhoneNumberFormat import LocalizedPeerData +import ContextUI private let dropDownIcon = { () -> UIImage in UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0) @@ -56,7 +57,7 @@ private final class ContactListNodeInteraction { fileprivate let activateSearch: () -> Void fileprivate let authorize: () -> Void fileprivate let suppressWarning: () -> Void - fileprivate let openPeer: (ContactListPeer, ContactListAction) -> Void + fileprivate let openPeer: (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void fileprivate let openDisabledPeer: (EnginePeer, ChatListDisabledPeerReason) -> Void fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void @@ -65,7 +66,7 @@ private final class ContactListNodeInteraction { let itemHighlighting = ContactItemHighlighting() - init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void) { + init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void) { self.activateSearch = activateSearch self.authorize = authorize self.suppressWarning = suppressWarning @@ -96,7 +97,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case permissionInfo(PresentationTheme, String, String, Bool) case permissionEnable(PresentationTheme, String) case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings) - case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, StoryData?, Bool) + case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, Bool, StoryData?, Bool) var stableId: ContactListNodeEntryId { switch self { @@ -110,7 +111,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return .permission(action: true) case let .option(index, _, _, _, _): return .option(index: index) - case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, storyData, _): + case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, _, storyData, _): switch peer { case let .peer(peer, _, _): return .peerId(peerId: peer.id.toInt64(), section: storyData != nil ? .stories : .contacts) @@ -143,7 +144,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { }) case let .option(_, option, header, _, _): return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action) - case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled, storyData, requiresPremiumForMessaging): + case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, hasMoreButton, enabled, storyData, requiresPremiumForMessaging): var status: ContactsPeerItemStatus let itemPeer: ContactsPeerItemPeer var isContextActionEnabled = false @@ -200,11 +201,15 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } var additionalActions: [ContactsPeerItemAction] = [] - if displayCallIcons { - additionalActions = [ContactsPeerItemAction(icon: .voiceCall, action: { _ in - interaction.openPeer(peer, .voiceCall) - }), ContactsPeerItemAction(icon: .videoCall, action: { _ in - interaction.openPeer(peer, .videoCall) + if hasMoreButton { + additionalActions = [ContactsPeerItemAction(icon: .more, action: { _, sourceNode, gesture in + interaction.openPeer(peer, .more, sourceNode, gesture) + })] + } else if displayCallIcons { + additionalActions = [ContactsPeerItemAction(icon: .voiceCall, action: { _, sourceNode, gesture in + interaction.openPeer(peer, .voiceCall, sourceNode, gesture) + }), ContactsPeerItemAction(icon: .videoCall, action: { _, sourceNode, gesture in + interaction.openPeer(peer, .videoCall, sourceNode, gesture) })] } @@ -218,7 +223,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in - interaction.openPeer(peer, .generic) + interaction.openPeer(peer, .generic, nil, nil) }, disabledAction: { _ in if case let .peer(peer, _, _) = peer { interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) @@ -263,9 +268,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled, lhsStoryData, lhsRequiresPremiumForMessaging): + case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsHasMoreButton, lhsEnabled, lhsStoryData, lhsRequiresPremiumForMessaging): switch rhs { - case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled, rhsStoryData, rhsRequiresPremiumForMessaging): + case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsHasMoreButton, rhsEnabled, rhsStoryData, rhsRequiresPremiumForMessaging): if lhsIndex != rhsIndex { return false } @@ -303,6 +308,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if lhsDisplayCallIcons != rhsDisplayCallIcons { return false } + if lhsHasMoreButton != rhsHasMoreButton { + return false + } if lhsEnabled != rhsEnabled { return false } @@ -353,11 +361,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case .peer: return true } - case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData, _): + case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData, _): switch rhs { case .search, .sort, .permissionInfo, .permissionEnable, .option: return false - case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData, _): + case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData, _): if (lhsStoryData == nil) != (rhsStoryData == nil) { if lhsStoryData != nil { return true @@ -551,7 +559,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false)) index += 1 } @@ -608,7 +616,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, true, nil, false)) index += 1 } @@ -657,7 +665,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false)) index += 1 } @@ -701,7 +709,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis enabled = true } - entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil, false)) + entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, false)) index += 1 } } @@ -748,7 +756,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } - entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil, requiresPremiumForMessaging)) + entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, requiresPremiumForMessaging)) index += 1 } return entries @@ -772,7 +780,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta case .search: //indexSections.apend(CollectionIndexNode.searchIndex) break - case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _): + case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if !existingSections.contains(header.letter) { existingSections.insert(header.letter) @@ -1036,7 +1044,7 @@ public final class ContactListNode: ASDisplayNode { public var contentScrollingEnded: ((ListView) -> Bool)? public var activateSearch: (() -> Void)? - public var openPeer: ((ContactListPeer, ContactListAction) -> Void)? + public var openPeer: ((ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void)? public var openDisabledPeer: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? public var deselectedAll: (() -> Void)? public var updatedSelection: (([EnginePeer], Bool) -> Void)? @@ -1136,7 +1144,7 @@ public final class ContactListNode: ASDisplayNode { authorizeImpl?() }, suppressWarning: { [weak self] in self?.suppressPermissionWarning?() - }, openPeer: { [weak self] peer, action in + }, openPeer: { [weak self] peer, action, sourceNode, gesture in if let strongSelf = self { if strongSelf.multipleSelection { var updated = false @@ -1151,10 +1159,10 @@ public final class ContactListNode: ASDisplayNode { } }) if !updated { - strongSelf.openPeer?(peer, action) + strongSelf.openPeer?(peer, action, sourceNode, gesture) } } else { - strongSelf.openPeer?(peer, action) + strongSelf.openPeer?(peer, action, sourceNode, gesture) } } }, openDisabledPeer: { [weak self] peer, reason in @@ -1224,7 +1232,7 @@ public final class ContactListNode: ASDisplayNode { strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: index, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } - case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _): + case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if let scalar = UnicodeScalar(header.letter) { let title = "\(Character(scalar))" diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index c79c7a5bb5..593a022af4 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -345,7 +345,7 @@ public class ContactsController: ViewController { self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, _ in + self.contactsNode.contactListNode.openPeer = { [weak self] peer, _, _, _ in guard let self else { return } diff --git a/submodules/ContactsPeerItem/BUILD b/submodules/ContactsPeerItem/BUILD index 66af4b82ba..921376ec47 100644 --- a/submodules/ContactsPeerItem/BUILD +++ b/submodules/ContactsPeerItem/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/MoreButtonNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index aee623f148..c504913c2c 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -20,6 +20,7 @@ import ComponentFlow import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent +import MoreButtonNode public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -88,13 +89,14 @@ public enum ContactsPeerItemActionIcon { case add case voiceCall case videoCall + case more } public struct ContactsPeerItemAction { public let icon: ContactsPeerItemActionIcon - public let action: ((ContactsPeerItemPeer) -> Void)? + public let action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)? - public init(icon: ContactsPeerItemActionIcon, action: @escaping (ContactsPeerItemPeer) -> Void) { + public init(icon: ContactsPeerItemActionIcon, action: @escaping (ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void) { self.icon = icon self.action = action } @@ -417,6 +419,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var badgeTextNode: TextNode? private var selectionNode: CheckNode? private var actionButtonNodes: [HighlightableButtonNode]? + private var moreButtonNode: MoreButtonNode? private var arrowButtonNode: HighlightableButtonNode? private var avatarTapRecognizer: UITapGestureRecognizer? @@ -744,10 +747,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var actionButtons: [ActionButton]? struct ActionButton { + let type: ContactsPeerItemActionIcon let image: UIImage? - let action: ((ContactsPeerItemPeer) -> Void)? + let action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)? - init(theme: PresentationTheme, icon: ContactsPeerItemActionIcon, action: ((ContactsPeerItemPeer) -> Void)?) { + init(theme: PresentationTheme, icon: ContactsPeerItemActionIcon, action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)?) { let image: UIImage? switch icon { case .none: @@ -758,7 +762,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { image = PresentationResourcesItemList.voiceCallIcon(theme) case .videoCall: image = PresentationResourcesItemList.videoCallIcon(theme) + case .more: + image = PresentationResourcesItemList.videoCallIcon(theme) } + self.type = icon self.image = image self.action = action } @@ -1357,7 +1364,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { verifiedIconView.removeFromSuperview() } - if let actionButtons = actionButtons { + if let actionButtons, actionButtons.count == 1, let actionButton = actionButtons.first, case .more = actionButton.type { + let moreButtonNode: MoreButtonNode + if let current = strongSelf.moreButtonNode { + moreButtonNode = current + } else { + moreButtonNode = MoreButtonNode(theme: item.presentationData.theme) + moreButtonNode.iconNode.enqueueState(.more, animated: false) + moreButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + strongSelf.offsetContainerNode.addSubnode(moreButtonNode) + strongSelf.moreButtonNode = moreButtonNode + } + moreButtonNode.action = { sourceNode, gesture in + actionButton.action?(item.peer, sourceNode, gesture) + } + let moreButtonSize = moreButtonNode.measure(CGSize(width: 100.0, height: nodeLayout.contentSize.height)) + moreButtonNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 18.0 - moreButtonSize.width, y:floor((nodeLayout.contentSize.height - moreButtonSize.height) / 2.0)), size: moreButtonSize) + } else if let actionButtons = actionButtons { if strongSelf.actionButtonNodes == nil { var actionButtonNodes: [HighlightableButtonNode] = [] for action in actionButtons { @@ -1524,7 +1547,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), let item = self.item, index < item.additionalActions.count else { return } - item.additionalActions[index].action?(item.peer) + item.additionalActions[index].action?(item.peer, sender, nil) } override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 11d90442d4..03b47bfce2 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1599,7 +1599,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.setBaseRate(self.playbackRate ?? 1.0) } } else { - if self.shouldAutoplayOnCentrality() { + if isAnimated { + self.playOnContentOwnership = true + } else if self.shouldAutoplayOnCentrality() { self.playOnContentOwnership = true } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 9b0c5dc3d6..c36a897a91 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1592,11 +1592,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if case let .noAccess(cameraAccess) = self.state { var hasCamera = cameraAccess == .authorized var story = false - if let subject = self.controller?.subject, case .assets(_, .story) = subject { - hasCamera = false - story = true - - self.controller?.navigationItem.rightBarButtonItem = nil + if let subject = self.controller?.subject { + if case .assets(_, .story) = subject { + hasCamera = false + story = true + self.controller?.navigationItem.rightBarButtonItem = nil + } else if case .assets(_, .createSticker) = subject { + hasCamera = false + } } var placeholderTransition = transition diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 61ef685756..5e54ec3215 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -166,7 +166,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { } let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor if entities.count > 0 { - messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) } else { messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 3c90c168f1..0562d0fb63 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -112,6 +112,10 @@ private final class MediaCutoutScreenComponent: Component { x: location.x / controller.drawingView.bounds.width, y: location.y / controller.drawingView.bounds.height ) + let validRange: Range = 0.0 ..< 1.0 + guard validRange.contains(point.x) && validRange.contains(point.y) else { + return + } component.mediaEditor.processImage { [weak self] originalImage, _ in cutoutImage(from: originalImage, values: nil, target: .point(point), includeExtracted: false, completion: { [weak self] results in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 7e90ee8700..25cddca6d5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1177,8 +1177,10 @@ final class MediaEditorScreenComponent: Component { let authorName = forwardAuthor.displayTitle(strings: environment.strings, displayOrder: .firstLast) header = AnyComponent( ForwardInfoPanelComponent( + context: component.context, authorName: authorName, text: forwardStory.text, + entities: forwardStory.entities, isChannel: forwardAuthor.id.isGroupOrChannel, isVibrant: true, fillsWidth: true diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 2b541d9421..df26149917 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -228,20 +228,48 @@ func findEdgePoints(in pixelBuffer: CVPixelBuffer) -> [CGPoint] { return pixel >= 235 } - let directions = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] - var lastDirectionIndex = 0 - var startPoint: Point? = nil -outerLoop: for y in 0..() + var componentSize = 0 + + func floodFill(from point: Point) -> Int { + var stack = [point] + var size = 0 + + while let current = stack.popLast() { + let x = Int(current.x) + let y = Int(current.y) + + if x < 0 || x >= width || y < 0 || y >= height || visited.contains(current) || !isPixelWhiteAt(x: x, y: y) { + continue + } + + visited.insert(current) + size += 1 + stack.append(contentsOf: [Point(x: x+1, y: y), Point(x: x-1, y: y), Point(x: x, y: y+1), Point(x: x, y: y-1)]) + } + + return size + } + + for y in 0.. componentSize { + componentSize = size + startPoint = point + } } } } - guard let startingPoint = startPoint else { return [] } + let directions = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] + var lastDirectionIndex = 0 + + guard let startingPoint = startPoint, componentSize > 60 else { return [] } + edgePoints.insert(startingPoint) edgePath.append(startingPoint) var currentPoint = startingPoint @@ -326,7 +354,7 @@ private func getEdgesBitmap(_ ciImage: CIImage) -> CVPixelBuffer? { context.fill(CGRect(origin: .zero, size: size)) image.draw(in: CGRect(origin: .zero, size: size)) UIGraphicsPopContext() - + return buffer } diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 1237bcd3f0..c293262100 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1365,7 +1365,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { contactListNode.activateSearch = { [weak self] in self?.requestActivateSearch?() } - contactListNode.openPeer = { [weak self] peer, _ in + contactListNode.openPeer = { [weak self] peer, _, _, _ in if case let .peer(peer, _, _) = peer { self?.contactListNode?.listNode.clearHighlightAnimated(true) self?.requestOpenPeer?(EnginePeer(peer), nil) diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD index f17f609005..ae6df8bf44 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD @@ -12,9 +12,13 @@ swift_library( deps = [ "//submodules/Display", "//submodules/ComponentFlow", + "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView", + "//submodules/AccountContext", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift index c1f065c7f1..170788ca40 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift @@ -2,25 +2,35 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramCore import MultilineTextComponent +import MultilineTextWithEntitiesComponent import MessageInlineBlockBackgroundView +import AccountContext +import TextFormat public final class ForwardInfoPanelComponent: Component { + public let context: AccountContext public let authorName: String public let text: String + public let entities: [MessageTextEntity] public let isChannel: Bool public let isVibrant: Bool public let fillsWidth: Bool public init( + context: AccountContext, authorName: String, text: String, + entities: [MessageTextEntity], isChannel: Bool, isVibrant: Bool, fillsWidth: Bool ) { + self.context = context self.authorName = authorName self.text = text + self.entities = entities self.isChannel = isChannel self.isVibrant = isVibrant self.fillsWidth = fillsWidth @@ -33,6 +43,9 @@ public final class ForwardInfoPanelComponent: Component { if lhs.text != rhs.text { return false } + if lhs.entities != rhs.entities { + return false + } if lhs.isChannel != rhs.isChannel { return false } @@ -47,7 +60,6 @@ public final class ForwardInfoPanelComponent: Component { public final class View: UIView { public let backgroundView: UIImageView -// private let blurBackgroundView: BlurredBackgroundView private let blurBackgroundView: UIVisualEffectView private let blockView: MessageInlineBlockBackgroundView private var iconView: UIImageView? @@ -58,8 +70,6 @@ public final class ForwardInfoPanelComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { -// self.blurBackgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.4)) - if #available(iOS 13.0, *) { self.blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) } else { @@ -102,7 +112,7 @@ public final class ForwardInfoPanelComponent: Component { iconView.frame = CGRect(origin: CGPoint(x: sideInset + UIScreenPixel, y: 5.0), size: image.size) } titleOffset += 13.0 - + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -124,14 +134,20 @@ public final class ForwardInfoPanelComponent: Component { view.frame = titleFrame } + let textFont = Font.regular(14.0) + let textColor = UIColor.white + let attributedText = stringWithAppliedEntities(component.text, entities: component.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil) + let textSize = self.text.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.text, - font: Font.regular(14.0), - textColor: .white - )), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4), + text: .plain(attributedText), + horizontalAlignment: .natural, + truncationType: .end, maximumNumberOfLines: 1 )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 7bc8d4a842..5c8132efaa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -698,6 +698,7 @@ final class StoryContentCaptionComponent: Component { let authorName: String let isChannel: Bool let text: String? + let entities: [MessageTextEntity] switch forwardInfo { case let .known(peer, _, _): @@ -706,6 +707,7 @@ final class StoryContentCaptionComponent: Component { if let story = self.forwardInfoStory { text = story.text + entities = story.entities } else if self.forwardInfoDisposable == nil, let forwardInfoStory = component.forwardInfoStory { self.forwardInfoDisposable = (forwardInfoStory |> deliverOnMainQueue).start(next: { story in @@ -717,13 +719,16 @@ final class StoryContentCaptionComponent: Component { } }) text = "" + entities = [] } else { text = "" + entities = [] } case let .unknown(name, _): authorName = name isChannel = false text = "" + entities = [] } if let text { @@ -741,8 +746,10 @@ final class StoryContentCaptionComponent: Component { PlainButtonComponent( content: AnyComponent( ForwardInfoPanelComponent( + context: component.context, authorName: authorName, text: text, + entities: entities, isChannel: isChannel, isVibrant: false, fillsWidth: false diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 42dee71c53..229268d614 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -1164,6 +1164,8 @@ extension ChatControllerImpl { controller.openCamera = { [weak self] cameraView in if let cameraView = cameraView as? TGAttachmentCameraView { self?.openCamera(cameraView: cameraView) + } else { + self?.openCamera(cameraView: nil) } } controller.presentWebSearch = { [weak self, weak controller] mediaGroups, activateOnDisplay in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f65cf5bfbe..b2d77e02dd 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -670,10 +670,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if messages.count == 1 { for media in messages[0].media { if let file = media as? TelegramMediaFile { - for attribute in file.attributes { - if case let .Sticker(_, packInfo, _) = attribute, packInfo != nil { - loadStickerSaveStatus = file.fileId - } + if file.isSticker { + loadStickerSaveStatus = file.fileId } if loadStickerSaveStatus == nil { loadCopyMediaResource = file.resource diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index 881560b309..4d0571e71f 100644 --- a/submodules/TelegramUI/Sources/ComposeController.swift +++ b/submodules/TelegramUI/Sources/ComposeController.swift @@ -116,7 +116,7 @@ public class ComposeControllerImpl: ViewController, ComposeController { self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, _ in + self.contactsNode.contactListNode.openPeer = { [weak self] peer, _, _, _ in if case let .peer(peer, _, _) = peer { self?.openPeer(peerId: peer.id) } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index a21c3faef0..bc4fa72e14 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -16,6 +16,7 @@ import CounterContollerTitleView import EditableTokenListNode import PremiumUI import UndoUI +import ContextUI private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { if peer.id == accountPeerId { @@ -415,6 +416,38 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } + self.contactsNode.openPeerMore = { [weak self] peer, node, gesture in + guard let self, case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else { + return + } + + let presentationData = self.presentationData + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.stings.Premium_Gift_ContactSelection_SendMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.params.sendMessage?(EnginePeer(peer)) + } + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.stings.Premium_Gift_ContactSelection_OpenProfile, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.params.openProfile?(EnginePeer(peer)) + } + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.present(contextController, in: .window(.root)) + } + self.contactsNode.openDisabledPeer = { [weak self] peer, reason in guard let self else { return @@ -765,3 +798,17 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self._result.set(.single(.result(peerIds: peerIds, additionalOptionIds: additionalOptionIds))) } } + +private final class ContactContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 4929a3a737..93377b49e7 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -13,6 +13,7 @@ import AnimationCache import MultiAnimationRenderer import EditableTokenListNode import SolidRoundedButtonNode +import ContextUI private struct SearchResultEntry: Identifiable { let index: Int @@ -58,6 +59,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeerId) -> Void)? var openPeer: ((ContactListPeer) -> Void)? + var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? var openDisabledPeer: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? var removeSelectedPeer: ((ContactListPeerId) -> Void)? var removeSelectedCategory: ((Int) -> Void)? @@ -262,8 +264,12 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { switch self.contentNode { case let .contacts(contactsNode): - contactsNode.openPeer = { [weak self] peer, _ in - self?.openPeer?(peer) + contactsNode.openPeer = { [weak self] peer, action, sourceNode, gesture in + if case .more = action { + self?.openPeerMore?(peer, sourceNode, gesture) + } else { + self?.openPeer?(peer) + } } contactsNode.openDisabledPeer = { [weak self] peer, reason in guard let self else { @@ -362,7 +368,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { globalSearch: globalSearch, displaySavedMessages: displaySavedMessages ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) - searchResultsNode.openPeer = { peer, _ in + searchResultsNode.openPeer = { peer, _, _, _ in self?.tokenListNode.setText("") self?.openPeer?(peer) } diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 8fee5a2f58..4cae4dae53 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -197,7 +197,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, action in + self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, _, _ in self?.openPeer(peer: peer, action: action) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 6a777d9082..d530187c0b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2164,15 +2164,33 @@ public final class SharedAccountContextImpl: SharedAccountContext { mode = .premiumGifting(birthdays: nil, selectToday: false) } - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: mode, options: [], isPeerEnabled: { peer in - if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { - return true - } else { - return false - } - }, limit: limit, reachedLimit: { limit in - reachedLimitImpl?(limit) - })) + var openProfileImpl: ((EnginePeer) -> Void)? + var sendMessageImpl: ((EnginePeer) -> Void)? + + let controller = context.sharedContext.makeContactMultiselectionController( + ContactMultiselectionControllerParams( + context: context, + mode: mode, + options: [], + isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { + return true + } else { + return false + } + }, + limit: limit, + reachedLimit: { limit in + reachedLimitImpl?(limit) + }, + openProfile: { peer in + openProfileImpl?(peer) + }, + sendMessage: { peer in + sendMessageImpl?(peer) + } + ) + ) reachedLimitImpl = { [weak controller] limit in guard let controller else { @@ -2227,6 +2245,36 @@ public final class SharedAccountContextImpl: SharedAccountContext { controller.push(giftController) }) + sendMessageImpl = { [weak self, weak controller] peer in + guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + self.navigateToChatController( + NavigateToChatControllerParams( + navigationController: navigationController, + context: context, + chatLocation: .peer(peer) + ) + ) + } + + openProfileImpl = { [weak self, weak controller] peer in + guard let self, let controller else { + return + } + if let infoController = self.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: true, + fromChat: false, + requestsContext: nil + ) { + controller.replace(with: infoController) + } + } + return controller }