From ec8bd9acdd7853f36753ffa48f1434437fbadf56 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 4 Jun 2026 01:44:36 +0200 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 + .../Sources/AttachmentPanel.swift | 2 +- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListController.swift | 28 +- .../ChatListFilterPresetListController.swift | 40 +-- .../Sources/ChatListSearchContainerNode.swift | 59 ++-- .../Sources/ChatListSearchListPaneNode.swift | 12 +- .../Sources/Node/ChatListItem.swift | 4 +- .../ChatPanelInterfaceInteraction.swift | 6 +- .../GalleryUI/Sources/GalleryTitleView.swift | 58 ++- submodules/PeerAvatarGalleryUI/BUILD | 3 + .../Sources/AvatarGalleryController.swift | 333 ++++++++++-------- .../AvatarGalleryItemFooterContentNode.swift | 206 ++++++----- .../Sources/PeerAvatarImageGalleryItem.swift | 130 +++++-- .../Sources/SecretChatKeyController.swift | 3 +- .../ChatMessageSelectionInputPanelNode.swift | 2 +- .../Sources/ChatRecentActionsController.swift | 2 +- .../Sources/ChatTextInputPanelComponent.swift | 2 +- ...nfoScreenDisclosureEncryptionKeyItem.swift | 6 +- .../Sources/PeerInfoSelectionPanelNode.swift | 2 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../Chat/ChatControllerLoadDisplayNode.swift | 4 +- ...ChatControllerOpenMessageContextMenu.swift | 6 +- .../Sources/ChatControllerAdminBanUsers.swift | 26 +- 24 files changed, 570 insertions(+), 371 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 43e6681e57..15ee958e27 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16355,3 +16355,7 @@ Error: %8$@"; "Chat.ContextMenu.OpenInApp" = "Open In-App"; "CreatePoll.Link.Description" = "Attach a link to this option."; + +"ChatList.ClearSearchHistory.Confirm" = "Clear"; + +"ChatList.RemoveFolderConfirmationTitle" = "Remove %@?"; diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 340a97f14a..1927dd3991 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1125,7 +1125,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in - }, deleteSelectedMessages: { + }, deleteSelectedMessages: { _ in }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index ed6f05ff25..250bf011a9 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/SearchBarNode:SearchBarNode", "//submodules/ChatListSearchRecentPeersNode:ChatListSearchRecentPeersNode", "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", + "//submodules/TelegramUI/Components/SectionTitleContextItem", "//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager", "//submodules/PeerPresenceStatusManager:PeerPresenceStatusManager", "//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d02ebd71aa..afb7a667c8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -4487,11 +4487,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let self else { return } - guard let filter = filters.first(where: { $0.id == id }) else { + guard let filter = filters.first(where: { $0.id == id }), case let .filter(_, title, _, data) = filter else { return } - if case let .filter(_, title, _, data) = filter, data.isShared { + if data.isShared { let _ = (combineLatest( self.context.engine.data.get( EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), @@ -4573,24 +4573,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) } else { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: self.presentationData.strings.ChatList_RemoveFolderConfirmation), - ActionSheetButtonItem(title: self.presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - apply() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) + let alertController = textAlertController(context: context, title: self.presentationData.strings.ChatList_RemoveFolderConfirmationTitle(title.text).string, text: self.presentationData.strings.ChatList_RemoveFolderConfirmation, actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.ChatList_RemoveFolderAction, action: { + apply() + }) ]) - self.present(actionSheet, in: .window(.root)) + self.present(alertController, in: .window(.root)) } }) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index f4817f217f..c37f487cdb 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -461,13 +461,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let _ = (context.engine.peers.currentChatListFilters() |> take(1) |> deliverOnMainQueue).start(next: { filters in - guard let filter = filters.first(where: { $0.id == id }) else { + guard let filter = filters.first(where: { $0.id == id }), case let .filter(_, title, _, data) = filter else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - if case let .filter(_, title, _, data) = filter, data.isShared { + if data.isShared { let _ = (combineLatest( context.engine.data.get( EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), @@ -539,31 +539,21 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } }) } else { - let actionSheet = ActionSheetController(presentationData: presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.ChatList_RemoveFolderConfirmation), - ActionSheetButtonItem(title: presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - if let index = filters.firstIndex(where: { $0.id == id }) { - filters.remove(at: index) - } - return filters + let alertController = textAlertController(context: context, title: presentationData.strings.ChatList_RemoveFolderConfirmationTitle(title.text).string, text: presentationData.strings.ChatList_RemoveFolderConfirmation, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: presentationData.strings.ChatList_RemoveFolderAction, action: { + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + if let index = filters.firstIndex(where: { $0.id == id }) { + filters.remove(at: index) } - |> deliverOnMainQueue).startStandalone() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) + return filters + } + |> deliverOnMainQueue).startStandalone() + }) ]) - presentControllerImpl?(actionSheet) + presentControllerImpl?(alertController) } }) }, updateDisplayTags: { value in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 65b39eb784..1cae0c3f76 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -35,6 +35,7 @@ import PremiumUI import AvatarNode import StoryContainerScreen import ChatListSearchFiltersContainerNode +import SectionTitleContextItem import EdgeEffect import ComponentFlow import ComponentDisplayAdapters @@ -53,7 +54,7 @@ final class ChatListSearchInteraction { let openDisabledPeer: (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void let openMessage: (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void let openUrl: (String) -> Void - let clearRecentSearch: () -> Void + let clearRecentSearch: (ASDisplayNode) -> Void let addContact: (String) -> Void let toggleMessageSelection: (EngineMessage.Id, Bool) -> Void let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void) @@ -67,7 +68,7 @@ final class ChatListSearchInteraction { let dismissSearch: () -> Void let openAdInfo: (ASDisplayNode, AdPeer) -> Void - init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((EnginePeer.Id, ASDisplayNode) -> Void)?, switchToFilter: @escaping (ChatListSearchPaneKey) -> Void, dismissSearch: @escaping () -> Void, openAdInfo: @escaping (ASDisplayNode, AdPeer) -> Void) { + init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping (ASDisplayNode) -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((EnginePeer.Id, ASDisplayNode) -> Void)?, switchToFilter: @escaping (ChatListSearchPaneKey) -> Void, dismissSearch: @escaping () -> Void, openAdInfo: @escaping (ASDisplayNode, AdPeer) -> Void) { self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.openMessage = openMessage @@ -88,6 +89,20 @@ final class ChatListSearchInteraction { } } +private final class ChatListSearchClearRecentReferenceContentSource: ContextReferenceContentSource { + private let sourceNode: ASDisplayNode + + let keepInPlace: Bool = true + + init(sourceNode: ASDisplayNode) { + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .bottom) + } +} + private struct ChatListSearchContainerNodeSearchState: Equatable { var selectedMessageIds: Set? @@ -241,30 +256,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self?.dismissInput() }, contentContext: nil, progress: nil, completion: nil) }, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil) - }, clearRecentSearch: { [weak self] in + }, clearRecentSearch: { [weak self] sourceNode in guard let strongSelf = self else { return } + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil let presentationData = strongSelf.presentationData - let actionSheet = ActionSheetController(presentationData: presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.ChatList_ClearSearchHistory), - ActionSheetButtonItem(title: presentationData.strings.WebSearch_RecentSectionClear, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: presentationData.strings.ChatList_ClearSearchHistory, textFont: .small, icon: { _ in return nil }, action: emptyAction)), + .action(ContextMenuActionItem(text: presentationData.strings.ChatList_ClearSearchHistory_Confirm, textColor: .destructive, icon: { theme in + return nil + }, action: { [weak self] c, _ in + let clearImpl = { [weak self] in + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.engine.peers.clearRecentlySearchedPeers() + |> deliverOnMainQueue).startStandalone() } - let _ = (strongSelf.context.engine.peers.clearRecentlySearchedPeers() - |> deliverOnMainQueue).startStandalone() - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) + if let c { + c.dismiss(completion: clearImpl) + } else { + clearImpl() + } + })) + ] + let controller = makeContextController(presentationData: presentationData, source: .reference(ChatListSearchClearRecentReferenceContentSource(sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) strongSelf.dismissInput() - strongSelf.present?(actionSheet, nil) + strongSelf.present?(controller, nil) }, addContact: { phoneNumber in addContact?(phoneNumber) }, toggleMessageSelection: { [weak self] messageId, selected in diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 498ebd2303..76ea9c9c4f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -126,7 +126,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { peerSelected: @escaping (EnginePeer, Int64?, Bool, OpenPeerAction) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, - clearRecentlySearchedPeers: @escaping () -> Void, + clearRecentlySearchedPeers: @escaping (ASDisplayNode) -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, @@ -294,8 +294,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } else if case .globalPosts = key { header = ChatListSearchItemHeader(type: .text(strings.ChatList_HeaderPublicPosts, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } else { - header = ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { _ in - clearRecentlySearchedPeers() + header = ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear, action: { sourceNode in + clearRecentlySearchedPeers(sourceNode) }) } @@ -1334,7 +1334,7 @@ private func chatListSearchContainerPreparedRecentTransition( peerSelected: @escaping (EnginePeer, Int64?, Bool, OpenPeerAction) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, - clearRecentlySearchedPeers: @escaping () -> Void, + clearRecentlySearchedPeers: @escaping (ASDisplayNode) -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, @@ -4524,8 +4524,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else { gesture?.cancel() } - }, clearRecentlySearchedPeers: { - interaction.clearRecentSearch() + }, clearRecentlySearchedPeers: { sourceNode in + interaction.clearRecentSearch(sourceNode) }, deletePeer: { peerId in let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).startStandalone() }, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer, openStories: { peerId, avatarNode in diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 4ca41f47df..67257066fe 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -5096,11 +5096,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { nextTitleIconOrigin += 7.0 let titleBadgeFrame = CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: titleFrame.minY + floor((titleFrame.height - titleBadgeLayout.size.height) * 0.5)), size: titleBadgeLayout.size) nextTitleIconOrigin += titleBadgeLayout.size.width + 4.0 - titleBadgeNode.frame = titleBadgeFrame + transition.updateFrame(node: titleBadgeNode, frame: titleBadgeFrame) var titleBadgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -4.0, dy: -2.0) titleBadgeBackgroundFrame.size.height -= 1.0 - backgroundView.frame = titleBadgeBackgroundFrame + transition.updateFrame(view: backgroundView, frame: titleBadgeBackgroundFrame) if item.presentationData.theme.overallDarkAppearance { backgroundView.tintColor = theme.titleColor.withMultipliedAlpha(0.1) } else { diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 02e3c62d25..7a3b2fd374 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -74,7 +74,7 @@ public final class ChatPanelInterfaceInteraction { public let setupEditMessage: (EngineMessage.Id?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let beginMessageSelection: ([EngineMessage.Id], @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let cancelMessageSelection: (ContainedViewLayoutTransition) -> Void - public let deleteSelectedMessages: () -> Void + public let deleteSelectedMessages: (UIView?) -> Void public let reportSelectedMessages: () -> Void public let reportMessages: ([EngineRawMessage], ContextControllerProtocol?) -> Void public let blockMessageAuthor: (EngineRawMessage, ContextControllerProtocol?) -> Void @@ -206,7 +206,7 @@ public final class ChatPanelInterfaceInteraction { setupEditMessage: @escaping (EngineMessage.Id?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([EngineMessage.Id], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, cancelMessageSelection: @escaping (ContainedViewLayoutTransition) -> Void, - deleteSelectedMessages: @escaping () -> Void, + deleteSelectedMessages: @escaping (UIView?) -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([EngineRawMessage], ContextControllerProtocol?) -> Void, blockMessageAuthor: @escaping (EngineRawMessage, ContextControllerProtocol?) -> Void, @@ -475,7 +475,7 @@ public final class ChatPanelInterfaceInteraction { }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in - }, deleteSelectedMessages: { + }, deleteSelectedMessages: { _ in }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift index 01714785b3..c38569eda7 100644 --- a/submodules/GalleryUI/Sources/GalleryTitleView.swift +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -16,12 +16,24 @@ private let dateFont = Font.regular(12.0) public final class GalleryTitleView: UIView, NavigationBarTitleView { public final class Content: Equatable { - let message: EngineMessage + let message: EngineMessage? + let authorTitle: String? + let dateTitle: String? let title: String? let action: (() -> Void)? - + public init(message: EngineMessage, title: String?, action: (() -> Void)?) { self.message = message + self.authorTitle = nil + self.dateTitle = nil + self.title = title + self.action = action + } + + public init(authorTitle: String?, dateTitle: String?, title: String?, action: (() -> Void)?) { + self.message = nil + self.authorTitle = authorTitle + self.dateTitle = dateTitle self.title = title self.action = action } @@ -30,6 +42,12 @@ public final class GalleryTitleView: UIView, NavigationBarTitleView { if lhs.message != rhs.message { return false } + if lhs.authorTitle != rhs.authorTitle { + return false + } + if lhs.dateTitle != rhs.dateTitle { + return false + } if lhs.title != rhs.title { return false } @@ -114,18 +132,38 @@ public final class GalleryTitleView: UIView, NavigationBarTitleView { public func setContent(content: Content?) { self.content = content - self.backgroundContainer.isHidden = self.content == nil + let authorNameText: String? + let dateText: String? if let content { - let authorNameText = stringForFullAuthorName(message: content.message, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, accountPeerId: self.context.account.peerId).first ?? "" - let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: content.message.timestamp).string - - self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) - self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: UIColor(white: 1.0, alpha: 0.5)) + if let message = content.message { + authorNameText = stringForFullAuthorName(message: message, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, accountPeerId: self.context.account.peerId).first ?? "" + dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: message.timestamp).string + } else { + authorNameText = content.authorTitle + dateText = content.dateTitle + } + } else { + authorNameText = nil + dateText = nil } - + + if let authorNameText { + self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) + } else { + self.authorNameNode.attributedText = nil + } + + if let dateText { + self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: UIColor(white: 1.0, alpha: 0.5)) + } else { + self.dateNode.attributedText = nil + } + + self.backgroundContainer.isHidden = (authorNameText?.isEmpty ?? true) && (dateText?.isEmpty ?? true) + self.titleString = content?.title - + if !self.bounds.isEmpty { let _ = self.updateLayout(availableSize: self.bounds.size, transition: .immediate) } diff --git a/submodules/PeerAvatarGalleryUI/BUILD b/submodules/PeerAvatarGalleryUI/BUILD index 4afd265a5b..8a9a4c8983 100644 --- a/submodules/PeerAvatarGalleryUI/BUILD +++ b/submodules/PeerAvatarGalleryUI/BUILD @@ -12,6 +12,8 @@ swift_library( deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/ComponentFlow", + "//submodules/ContextUI:ContextUI", "//submodules/Display:Display", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", @@ -22,6 +24,7 @@ swift_library( "//submodules/PhotoResources:PhotoResources", "//submodules/RadialStatusNode:RadialStatusNode", "//submodules/ShareController:ShareController", + "//submodules/TelegramUI/Components/GlassControls", "//submodules/AppBundle:AppBundle", "//submodules/LegacyComponents:LegacyComponents", "//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI", diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 3b05051ac9..49f8e79494 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -8,11 +8,13 @@ import TelegramCore import TelegramPresentationData import AccountContext import GalleryUI +import ContextUI import LegacyComponents import LegacyMediaPickerUI import SaveToCameraRoll import OverlayStatusController import PresentationDataUtils +import AppBundle public enum AvatarGalleryEntryId: Hashable { case topImage @@ -20,6 +22,20 @@ public enum AvatarGalleryEntryId: Hashable { case resource(String) } +private final class AvatarGalleryContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + private let actionsOnTop: Bool + + init(sourceView: UIView, actionsOnTop: Bool = false) { + self.sourceView = sourceView + self.actionsOnTop = actionsOnTop + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom) + } +} + private func avatarGalleryEntryMatchesImage(_ entry: AvatarGalleryEntry, _ image: TelegramMediaImage) -> Bool { guard let entryRepresentation = largestImageRepresentation(entry.representations.map({ $0.representation })), @@ -535,12 +551,12 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: sourceCorners, delete: strongSelf.canDelete ? { - self?.deleteEntry(entry) + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: sourceCorners, delete: strongSelf.canDelete ? { [weak self] sourceView in + self?.presentDeleteEntryConfirmation(entry, sourceView: sourceView, gesture: nil) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) - }, edit: { [weak self] in - self?.editEntry(entry) + }, edit: { [weak self] sourceView, gesture in + self?.editEntry(entry, sourceView: sourceView, gesture: gesture) }) }), centralItemIndex: strongSelf.centralEntryIndex, synchronous: !isFirstTime) @@ -704,12 +720,12 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } let presentationData = self.presentationData - self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] in - self?.deleteEntry(entry) + self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] sourceView in + self?.presentDeleteEntryConfirmation(entry, sourceView: sourceView, gesture: nil) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) - }, edit: { [weak self] in - self?.editEntry(entry) + }, edit: { [weak self] sourceView, gesture in + self?.editEntry(entry, sourceView: sourceView, gesture: gesture) }) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in @@ -823,12 +839,12 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private func replaceEntries(_ entries: [AvatarGalleryEntry]) { self.galleryNode.currentThumbnailContainerNode?.updateSynchronously = true - self.galleryNode.pager.replaceItems(entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: self.peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] in - self?.deleteEntry(entry) + self.galleryNode.pager.replaceItems(entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: self.peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] sourceView in + self?.presentDeleteEntryConfirmation(entry, sourceView: sourceView, gesture: nil) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) - }, edit: { [weak self] in - self?.editEntry(entry) + }, edit: { [weak self] sourceView, gesture in + self?.editEntry(entry, sourceView: sourceView, gesture: gesture) }) }), centralItemIndex: 0, synchronous: true) self.entries = entries self.galleryNode.currentThumbnailContainerNode?.updateSynchronously = false @@ -895,158 +911,189 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } } - private func editEntry(_ rawEntry: AvatarGalleryEntry) { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - let dismissAction: () -> Void = { [weak actionSheet] in - actionSheet?.dismissAnimated() - } - - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Settings_SetNewProfilePhotoOrVideo, color: .accent, action: { [weak self] in - dismissAction() - self?.openAvatarSetup?({ [weak self] in - self?.dismissImmediately() + private func editEntry(_ rawEntry: AvatarGalleryEntry, sourceView rawSourceView: UIView, gesture: ContextGesture?) { + let presentationData = self.presentationData + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: { [weak self] in + self?.openAvatarSetup?({ [weak self] in + self?.dismissImmediately() + }) }) - })) - + }))) + var isFallback = false if case let .image(_, _, _, _, _, _, _, _, _, _, isFallbackValue, _) = rawEntry { isFallback = isFallbackValue } - + if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 || isFallback { let title: String if let _ = rawEntry.videoRepresentations.last { - title = self.presentationData.strings.ProfilePhoto_SetMainVideo + title = presentationData.strings.ProfilePhoto_SetMainVideo } else { - title = self.presentationData.strings.ProfilePhoto_SetMainPhoto + title = presentationData.strings.ProfilePhoto_SetMainPhoto } - items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak self] in - dismissAction() - self?.setMainEntry(rawEntry) - })) + items.append(.action(ContextMenuActionItem(text: title, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: { [weak self] in + self?.setMainEntry(rawEntry) + }) + }))) } let deleteTitle: String if let _ = rawEntry.videoRepresentations.last { - deleteTitle = self.presentationData.strings.Settings_RemoveVideo + deleteTitle = presentationData.strings.Settings_RemoveVideo } else { - deleteTitle = self.presentationData.strings.GroupInfo_SetGroupPhotoDelete + deleteTitle = presentationData.strings.GroupInfo_SetGroupPhotoDelete } - items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak self] in - dismissAction() - self?.deleteEntry(rawEntry) - })) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - self.view.endEditing(true) - self.present(actionSheet, in: .window(.root)) - } - - private func deleteEntry(_ rawEntry: AvatarGalleryEntry) { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let proceed = { - var entry = rawEntry - if case .topImage = entry, !self.entries.isEmpty { - entry = self.entries[0] + items.append(.action(ContextMenuActionItem(text: deleteTitle, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] c, _ in + guard let self, let c else { + return } - - self.removedEntry?(rawEntry) - - var focusOnItem: Int? - var updatedEntries = self.entries - var replaceItems = false - var dismiss = false - - switch entry { - case .topImage: - if self.peer.id == self.context.account.peerId { + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: presentationData.strings.Settings_RemoveConfirmation, textColor: .destructive, icon: { _ in + return nil + }, action: { [weak self] c, _ in + if let c { + c.dismiss(completion: { [weak self] in + self?.performDeleteEntry(rawEntry) + }) } else { - if entry == self.entries.first { - let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() - dismiss = true - } else { - if let index = self.entries.firstIndex(of: entry) { - self.entries.remove(at: index) - self.galleryNode.pager.transaction(GalleryPagerTransaction(deleteItems: [index], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false)) - } - } - } - case let .image(_, reference, _, _, _, _, _, messageId, _, _, isFallback, _): - if self.peer.id == self.context.account.peerId { - if isFallback { - let _ = self.context.engine.accountData.updateFallbackPhoto(resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() - } else if let reference = reference { - let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start() - } - - if entry == self.entries.first { - dismiss = true - } else { - if let index = self.entries.firstIndex(of: entry) { - replaceItems = true - updatedEntries.remove(at: index) - focusOnItem = index - 1 - } - } - } else { - if let messageId = messageId { - let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: [messageId], type: .forEveryone).start() - } - - if entry == self.entries.first { - let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() - dismiss = true - } else { - if let index = self.entries.firstIndex(of: entry) { - replaceItems = true - updatedEntries.remove(at: index) - focusOnItem = index - 1 - } - } + self?.performDeleteEntry(rawEntry) } - } - - if replaceItems { - updatedEntries = normalizeEntries(updatedEntries) - self.galleryNode.pager.replaceItems(updatedEntries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: self.peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] in - self?.deleteEntry(entry) - } : nil, setMain: { [weak self] in - self?.setMainEntry(entry) - }, edit: { [weak self] in - self?.editEntry(entry) - }) }), centralItemIndex: focusOnItem, synchronous: true) - self.entries = updatedEntries - } - if dismiss { - self._hiddenMedia.set(.single(nil)) - Queue.mainQueue().after(0.2) { - self.dismiss(forceAway: true) + })) + ] + c.pushItems(items: .single(ContextController.Items(content: .list(items)))) + }))) + + self.view.endEditing(true) + + let sourceView = self.navigationBar?.navigationButtonContextContainer(sourceView: rawSourceView) ?? rawSourceView + let contextController = makeContextController( + presentationData: presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), + source: .reference(AvatarGalleryContextReferenceContentSource(sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: gesture + ) + self.presentInGlobalOverlay(contextController) + } + + private func presentDeleteEntryConfirmation(_ rawEntry: AvatarGalleryEntry, sourceView: UIView, gesture: ContextGesture?) { + self.view.endEditing(true) + + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: self.presentationData.strings.Settings_RemoveConfirmation, textColor: .destructive, icon: { _ in + return nil + }, action: { [weak self] c, _ in + if let c { + c.dismiss(completion: { [weak self] in + self?.performDeleteEntry(rawEntry) + }) + } else { + self?.performDeleteEntry(rawEntry) } - } else { - if let firstEntry = self.entries.first { - self._hiddenMedia.set(.single(firstEntry)) - } - } - } - let actionSheet = ActionSheetController(presentationData: presentationData) - let items: [ActionSheetItem] = [ - ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - proceed() - }) + })) ] - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.present(actionSheet, in: .window(.root)) + let contextController = makeContextController( + presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), + source: .reference(AvatarGalleryContextReferenceContentSource(sourceView: sourceView, actionsOnTop: true)), + items: .single(ContextController.Items(content: .list(items))), + gesture: gesture + ) + self.presentInGlobalOverlay(contextController) + } + + private func performDeleteEntry(_ rawEntry: AvatarGalleryEntry) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var entry = rawEntry + if case .topImage = entry, !self.entries.isEmpty { + entry = self.entries[0] + } + + self.removedEntry?(rawEntry) + + var focusOnItem: Int? + var updatedEntries = self.entries + var replaceItems = false + var dismiss = false + + switch entry { + case .topImage: + if self.peer.id == self.context.account.peerId { + } else { + if entry == self.entries.first { + let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() + dismiss = true + } else { + if let index = self.entries.firstIndex(of: entry) { + self.entries.remove(at: index) + self.galleryNode.pager.transaction(GalleryPagerTransaction(deleteItems: [index], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false)) + } + } + } + case let .image(_, reference, _, _, _, _, _, messageId, _, _, isFallback, _): + if self.peer.id == self.context.account.peerId { + if isFallback { + let _ = self.context.engine.accountData.updateFallbackPhoto(resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() + } else if let reference = reference { + let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start() + } + + if entry == self.entries.first { + dismiss = true + } else { + if let index = self.entries.firstIndex(of: entry) { + replaceItems = true + updatedEntries.remove(at: index) + focusOnItem = index - 1 + } + } + } else { + if let messageId = messageId { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: [messageId], type: .forEveryone).start() + } + + if entry == self.entries.first { + let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() + dismiss = true + } else { + if let index = self.entries.firstIndex(of: entry) { + replaceItems = true + updatedEntries.remove(at: index) + focusOnItem = index - 1 + } + } + } + } + + if replaceItems { + updatedEntries = normalizeEntries(updatedEntries) + self.galleryNode.pager.replaceItems(updatedEntries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: self.peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] sourceView in + self?.presentDeleteEntryConfirmation(entry, sourceView: sourceView, gesture: nil) + } : nil, setMain: { [weak self] in + self?.setMainEntry(entry) + }, edit: { [weak self] sourceView, gesture in + self?.editEntry(entry, sourceView: sourceView, gesture: gesture) + }) }), centralItemIndex: focusOnItem, synchronous: true) + self.entries = updatedEntries + } + if dismiss { + self._hiddenMedia.set(.single(nil)) + Queue.mainQueue().after(0.2) { + self.dismiss(forceAway: true) + } + } else { + if let firstEntry = self.entries.first { + self._hiddenMedia.set(.single(firstEntry)) + } + } } } diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index acae546563..fc01d9507f 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -4,19 +4,11 @@ import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit -import Photos import TelegramPresentationData -import TelegramUIPreferences -import TelegramStringFormatting import AccountContext import GalleryUI -import AppBundle - -private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) -private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) - -private let nameFont = Font.medium(15.0) -private let dateFont = Font.regular(14.0) +import ComponentFlow +import GlassControls enum AvatarGalleryItemFooterContent { case info @@ -27,24 +19,19 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { private let context: AccountContext private var presentationData: PresentationData private var strings: PresentationStrings - private var dateTimeFormat: PresentationDateTimeFormat - private let deleteButton: UIButton - private let actionButton: UIButton - private let nameNode: ASTextNode - private let dateNode: ASTextNode + private let buttonPanel = ComponentView() private let mainNode: ASTextNode private let setMainButton: HighlightableButtonNode - private var currentNameText: String? - private var currentDateText: String? private var currentTypeText: String? + private var displayActionButton: Bool = true private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat, CGFloat)? - var delete: (() -> Void)? { + var delete: ((UIView) -> Void)? { didSet { - self.deleteButton.isHidden = self.delete == nil + self.requestLayout?(.immediate) } } @@ -56,25 +43,6 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { self.context = context self.presentationData = presentationData self.strings = presentationData.strings - self.dateTimeFormat = presentationData.dateTimeFormat - - self.deleteButton = UIButton() - self.deleteButton.isHidden = true - - self.actionButton = UIButton() - - self.deleteButton.setImage(deleteImage, for: [.normal]) - self.actionButton.setImage(actionImage, for: [.normal]) - - self.nameNode = ASTextNode() - self.nameNode.maximumNumberOfLines = 1 - self.nameNode.isUserInteractionEnabled = false - self.nameNode.displaysAsynchronously = false - - self.dateNode = ASTextNode() - self.dateNode.maximumNumberOfLines = 1 - self.dateNode.isUserInteractionEnabled = false - self.dateNode.displaysAsynchronously = false self.setMainButton = HighlightableButtonNode() self.setMainButton.isHidden = true @@ -86,36 +54,18 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { super.init() - self.view.addSubview(self.deleteButton) - self.view.addSubview(self.actionButton) - - self.addSubnode(self.nameNode) - self.addSubnode(self.dateNode) self.addSubnode(self.setMainButton) self.addSubnode(self.mainNode) - self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) - self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside]) self.setMainButton.addTarget(self, action: #selector(self.setMainButtonPressed), forControlEvents: .touchUpInside) } func setEntry(_ entry: AvatarGalleryEntry, content: AvatarGalleryItemFooterContent) { - var nameText: String? - var dateText: String? var typeText: String? var buttonText: String? var canShare = true switch entry { - case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback, _): - if date != 0 || isFallback { - nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" - } - if let date = date, date != 0 { - dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).string - } else if isFallback { - dateText = !videoRepresentations.isEmpty ? self.strings.ProfilePhoto_PublicVideo : self.strings.ProfilePhoto_PublicPhoto - } - + case let .image(_, _, _, videoRepresentations, peer, _, _, _, _, _, _, _): if (!videoRepresentations.isEmpty) { typeText = self.strings.ProfilePhoto_MainVideo buttonText = self.strings.ProfilePhoto_SetMainVideo @@ -131,23 +81,6 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { break } - if self.currentNameText != nameText || self.currentDateText != dateText { - self.currentNameText = nameText - self.currentDateText = dateText - - if let nameText = nameText { - self.nameNode.attributedText = NSAttributedString(string: nameText, font: nameFont, textColor: .white) - } else { - self.nameNode.attributedText = nil - } - - if let dateText = dateText { - self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) - } else { - self.dateNode.attributedText = nil - } - } - if self.currentTypeText != typeText { self.currentTypeText = typeText @@ -159,17 +92,16 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { } } - self.actionButton.isHidden = !canShare + if self.displayActionButton != canShare { + self.displayActionButton = canShare + self.requestLayout?(.immediate) + } switch content { case .info: - self.nameNode.isHidden = false - self.dateNode.isHidden = false self.mainNode.isHidden = true self.setMainButton.isHidden = true case let .own(isMainPhoto): - self.nameNode.isHidden = true - self.dateNode.isHidden = true self.mainNode.isHidden = !isMainPhoto self.setMainButton.isHidden = isMainPhoto } @@ -179,52 +111,116 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { self.validLayout = (size, metrics, leftInset, rightInset, bottomInset, contentInset) let width = size.width - var panelHeight: CGFloat = 44.0 + bottomInset + var panelHeight: CGFloat = 54.0 + bottomInset panelHeight += contentInset - self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) - self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) - - let constrainedSize = CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude) - let nameSize = self.nameNode.measure(constrainedSize) - let dateSize = self.dateNode.measure(constrainedSize) - - if nameSize.height.isZero { - self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize) - } else { - let labelsSpacing: CGFloat = 0.0 - self.nameNode.frame = CGRect(origin: CGPoint(x: floor((width - nameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - nameSize.height - labelsSpacing) / 2.0)), size: nameSize) - self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - nameSize.height - labelsSpacing) / 2.0) + nameSize.height + labelsSpacing), size: dateSize) + var buttonPanelInsets = UIEdgeInsets() + buttonPanelInsets.left = 8.0 + buttonPanelInsets.right = 8.0 + buttonPanelInsets.bottom = bottomInset + 8.0 + if bottomInset <= 32.0 { + buttonPanelInsets.left += 18.0 + buttonPanelInsets.right += 18.0 } + let constrainedSize = CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude) + let controlsY = panelHeight - buttonPanelInsets.bottom - 44.0 + let mainSize = self.mainNode.measure(constrainedSize) - self.mainNode.frame = CGRect(origin: CGPoint(x: floor((width - mainSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - mainSize.height) / 2.0)), size: mainSize) - + self.mainNode.frame = CGRect(origin: CGPoint(x: floor((width - mainSize.width) / 2.0), y: controlsY + floor((44.0 - mainSize.height) / 2.0)), size: mainSize) + let mainButtonSize = self.setMainButton.measure(constrainedSize) - self.setMainButton.frame = CGRect(origin: CGPoint(x: floor((width - mainButtonSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - mainButtonSize.height) / 2.0)), size: mainButtonSize) + self.setMainButton.frame = CGRect(origin: CGPoint(x: floor((width - mainButtonSize.width) / 2.0), y: controlsY + floor((44.0 - mainButtonSize.height) / 2.0)), size: mainButtonSize) + + var leftControlItems: [GlassControlGroupComponent.Item] = [] + var rightControlItems: [GlassControlGroupComponent.Item] = [] + + if self.displayActionButton { + leftControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("forward"), + content: .icon("Chat/Input/Accessory Panels/MessageSelectionForward"), + action: { [weak self] in + guard let self else { + return + } + self.actionButtonPressed() + } + )) + } + + if self.delete != nil { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("delete"), + content: .icon("Chat/Input/Accessory Panels/MessageSelectionTrash"), + action: { [weak self] in + guard let self else { + return + } + self.deleteButtonPressed() + } + )) + } + + if leftControlItems.isEmpty && rightControlItems.isEmpty { + self.buttonPanel.view?.removeFromSuperview() + } else { + let buttonPanelSize = self.buttonPanel.update( + transition: ComponentTransition(transition), + component: AnyComponent(GlassControlPanelComponent( + theme: defaultDarkColorPresentationTheme, + leftItem: leftControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: leftControlItems, + background: .panel + ), + centralItem: nil, + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightControlItems, + background: .panel + ), + centerAlignmentIfPossible: true + )), + environment: {}, + containerSize: CGSize(width: size.width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0) + ) + let buttonPanelFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: panelHeight - buttonPanelInsets.bottom - buttonPanelSize.height), size: buttonPanelSize) + if let buttonPanelView = self.buttonPanel.view { + if buttonPanelView.superview == nil { + self.view.addSubview(buttonPanelView) + } + ComponentTransition(transition).setFrame(view: buttonPanelView, frame: buttonPanelFrame) + } + } return LayoutInfo(height: panelHeight, needsShadow: false) } override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) { - self.deleteButton.alpha = 1.0 - self.actionButton.alpha = 1.0 - self.nameNode.alpha = 1.0 - self.dateNode.alpha = 1.0 + if let buttonPanelView = self.buttonPanel.view { + buttonPanelView.alpha = 1.0 + } + self.mainNode.alpha = 1.0 self.setMainButton.alpha = 1.0 } override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - self.deleteButton.alpha = 0.0 - self.actionButton.alpha = 0.0 - self.nameNode.alpha = 0.0 - self.dateNode.alpha = 0.0 + if let buttonPanelView = self.buttonPanel.view { + buttonPanelView.alpha = 0.0 + } + self.mainNode.alpha = 0.0 self.setMainButton.alpha = 0.0 completion() } @objc private func deleteButtonPressed() { - self.delete?() + let sourceView: UIView + if let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View, let itemView = buttonPanelView.rightItemView?.itemView(id: AnyHashable("delete")) { + sourceView = itemView + } else if let buttonPanelView = self.buttonPanel.view { + sourceView = buttonPanelView + } else { + sourceView = self.view + } + self.delete?(sourceView) } @objc private func actionButtonPressed() { diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index d6fed42e2b..4fa5671393 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData +import TelegramStringFormatting import AccountContext import RadialStatusNode import PhotoResources @@ -50,11 +51,11 @@ class PeerAvatarImageGalleryItem: GalleryItem { let presentationData: PresentationData let entry: AvatarGalleryEntry let sourceCorners: AvatarGalleryController.SourceCorners - let delete: (() -> Void)? + let delete: ((UIView) -> Void)? let setMain: (() -> Void)? - let edit: (() -> Void)? + let edit: ((UIView, ContextGesture?) -> Void)? - init(context: AccountContext, peer: EnginePeer, presentationData: PresentationData, entry: AvatarGalleryEntry, sourceCorners: AvatarGalleryController.SourceCorners, delete: (() -> Void)?, setMain: (() -> Void)?, edit: (() -> Void)?) { + init(context: AccountContext, peer: EnginePeer, presentationData: PresentationData, entry: AvatarGalleryEntry, sourceCorners: AvatarGalleryController.SourceCorners, delete: ((UIView) -> Void)?, setMain: (() -> Void)?, edit: ((UIView, ContextGesture?) -> Void)?) { self.context = context self.peer = peer self.presentationData = presentationData @@ -68,10 +69,6 @@ class PeerAvatarImageGalleryItem: GalleryItem { func node(synchronous: Bool) -> GalleryItemNode { let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer, sourceCorners: self.sourceCorners) - if let indexData = self.entry.indexData { - node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string)) - } - node.setEntry(self.entry, synchronous: synchronous) node.footerContentNode.delete = self.delete node.footerContentNode.setMain = self.setMain @@ -82,9 +79,6 @@ class PeerAvatarImageGalleryItem: GalleryItem { func updateNode(node: GalleryItemNode, synchronous: Bool) { if let node = node as? PeerAvatarImageGalleryItemNode { - if let indexData = self.entry.indexData { - node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string)) - } let previousContentAnimations = node.imageNode.contentAnimations if synchronous { node.imageNode.contentAnimations = [] @@ -124,6 +118,63 @@ private class PeerAvatarImageGalleryContentNode: ASDisplayNode { } } +private final class AvatarGalleryEditButtonNode: HighlightableButtonNode { + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + + private let textNode: ASTextNode + + var contextAction: ((UIView, ContextGesture?) -> Void)? + + init(title: String) { + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.textNode = ASTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + self.textNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: .white) + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.addSubnode(self.textNode) + + self.containerNode.shouldBegin = { [weak self] _ in + guard let self else { + return false + } + return self.contextAction != nil + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let self else { + return + } + self.contextAction?(self.referenceNode.view, gesture) + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + let textSize = self.textNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) + let size = CGSize(width: ceil(textSize.width) + 20.0, height: 44.0) + let bounds = CGRect(origin: CGPoint(), size: size) + + self.containerNode.frame = bounds + self.referenceNode.frame = bounds + self.textNode.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize) + + return size + } + + @objc private func buttonPressed() { + self.contextAction?(self.referenceNode.view, nil) + } +} + final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private let presentationData: PresentationData @@ -140,6 +191,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate let _ready = Promise() fileprivate let _title = Promise() + fileprivate let _titleContent = Promise(nil) fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>() private let statusNodeContainer: HighlightableButtonNode @@ -151,7 +203,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { private var status: EngineMediaResource.FetchStatus? private let playbackStatusDisposable = MetaDisposable() - fileprivate var edit: (() -> Void)? + fileprivate var edit: ((UIView, ContextGesture?) -> Void)? init(context: AccountContext, presentationData: PresentationData, peer: EnginePeer, sourceCorners: AvatarGalleryController.SourceCorners) { self.context = context @@ -162,15 +214,17 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.contentNode = PeerAvatarImageGalleryContentNode() self.imageNode = TransformImageNode() self.footerContentNode = AvatarGalleryItemFooterContentNode(context: context, presentationData: presentationData) - + self.statusNodeContainer = HighlightableButtonNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.statusNode.isHidden = true super.init() - + + self._title.set(.single("")) + self.contentNode.addSubnode(self.imageNode) - + self.imageNode.contentAnimations = .subsequentUpdates self.imageNode.view.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true @@ -227,7 +281,36 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } - + + private func makeTitleContent(entry: AvatarGalleryEntry) -> GalleryTitleView.Content? { + var counterText: String? + if let indexData = entry.indexData { + counterText = self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string + } + + var authorTitle: String? + var dateTitle: String? + switch entry { + case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback, _): + if date != 0 || isFallback { + authorTitle = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + } + if let date = date, date != 0 { + dateTitle = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: date).string + } else if isFallback { + dateTitle = !videoRepresentations.isEmpty ? self.presentationData.strings.ProfilePhoto_PublicVideo : self.presentationData.strings.ProfilePhoto_PublicPhoto + } + default: + break + } + + if counterText == nil && authorTitle == nil && dateTitle == nil { + return nil + } + + return GalleryTitleView.Content(authorTitle: authorTitle, dateTitle: dateTitle, title: counterText, action: nil) + } + fileprivate func setEntry(_ entry: AvatarGalleryEntry, synchronous: Bool) { let previousRepresentations = self.entry?.representations let previousVideoRepresentations = self.entry?.videoRepresentations @@ -237,10 +320,17 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { var barButtonItems: [UIBarButtonItem] = [] let footerContent: AvatarGalleryItemFooterContent = .info if self.peer.id == self.context.account.peerId { - let rightBarButtonItem = UIBarButtonItem(title: entry.videoRepresentations.isEmpty ? self.presentationData.strings.Settings_EditPhoto : self.presentationData.strings.Settings_EditVideo, style: .plain, target: self, action: #selector(self.editPressed)) + let editTitle = entry.videoRepresentations.isEmpty ? self.presentationData.strings.Settings_EditPhoto : self.presentationData.strings.Settings_EditVideo + let editButtonNode = AvatarGalleryEditButtonNode(title: editTitle) + editButtonNode.contextAction = { [weak self] sourceView, gesture in + self?.edit?(sourceView, gesture) + } + let rightBarButtonItem = UIBarButtonItem(customDisplayNode: editButtonNode)! + rightBarButtonItem.accessibilityLabel = editTitle barButtonItems.append(rightBarButtonItem) } self._rightBarButtonItems.set(.single(barButtonItems)) + self._titleContent.set(.single(self.makeTitleContent(entry: entry))) self.footerContentNode.setEntry(entry, content: footerContent) @@ -590,6 +680,10 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { override func title() -> Signal { return self._title.get() } + + override func titleContent() -> Signal { + return self._titleContent.get() + } override func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> { return self._rightBarButtonItems.get() @@ -618,10 +712,6 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - @objc private func editPressed() { - self.edit?() - } - override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { return .single((self.footerContentNode, nil)) } diff --git a/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift b/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift index ebafd19083..6d0510add2 100644 --- a/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift +++ b/submodules/PeerInfoUI/Sources/SecretChatKeyController.swift @@ -24,9 +24,10 @@ public final class SecretChatKeyController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) self.navigationPresentation = .modal + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.title = self.presentationData.strings.EncryptionKey_Title diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index adb73ad6fd..01c8b818d0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -297,7 +297,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } @objc private func deleteButtonPressed() { - self.interfaceInteraction?.deleteSelectedMessages() + self.interfaceInteraction?.deleteSelectedMessages(self.deleteButton) } @objc private func reportButtonPressed() { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 6d6472272f..79db3fa7b9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -58,7 +58,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in - }, deleteSelectedMessages: { + }, deleteSelectedMessages: { _ in }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index c14cdb2137..580f8836c3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -407,7 +407,7 @@ public final class ChatTextInputPanelComponent: Component { }, cancelMessageSelection: { _ in }, - deleteSelectedMessages: { + deleteSelectedMessages: { _ in }, reportSelectedMessages: { }, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift index ed92e4af43..4a942dc0be 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift @@ -98,9 +98,9 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree let arrowInset: CGFloat = 18.0 - let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 12.0), size: textSize) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 16.0), size: textSize) - let height = textSize.height + 24.0 + let height = textSize.height + 32.0 if let arrowImage = PresentationResourcesItemList.disclosureArrowImage(presentationData.theme) { self.arrowNode.image = arrowImage @@ -118,7 +118,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree let hasTopCorners = hasCorners && topItem == nil let hasBottomCorners = hasCorners && bottomItem == nil - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) self.bottomSeparatorNode.isHidden = hasBottomCorners diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift index 77d67c0e4d..4a341bba9c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift @@ -47,7 +47,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in - }, deleteSelectedMessages: { + }, deleteSelectedMessages: { _ in deleteMessages() }, reportSelectedMessages: { reportMessages() diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 37a7ccd34c..5c44f3b4e1 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -387,7 +387,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in - }, deleteSelectedMessages: { + }, deleteSelectedMessages: { _ in }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 2687e1c212..ce60db61b6 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1842,7 +1842,7 @@ extension ChatControllerImpl { return } self.updateChatPresentationInterfaceState(transition: transition, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - }, deleteSelectedMessages: { [weak self] in + }, deleteSelectedMessages: { [weak self] sourceView in if let strongSelf = self { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false) @@ -1856,7 +1856,7 @@ extension ChatControllerImpl { if actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty { strongSelf.presentClearCacheSuggestion() } else { - strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options, contextController: nil, completion: { _ in }) + strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options, contextController: nil, sourceView: sourceView, completion: { _ in }) } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 1ee89a7565..e6eff30b34 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -687,15 +687,17 @@ final class ChatControllerContextReferenceContentSource: ContextReferenceContent let sourceView: UIView let insets: UIEdgeInsets let contentInsets: UIEdgeInsets + let actionsOnTop: Bool - init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets(), actionsOnTop: Bool = false) { self.controller = controller self.sourceView = sourceView self.insets = insets self.contentInsets = contentInsets + self.actionsOnTop = actionsOnTop } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets, actionsPosition: self.actionsOnTop ? .top : .bottom) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index 778b109a5d..9e70344c1b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import TelegramPresentationData import AccountContext import TelegramCore @@ -439,7 +440,7 @@ extension ChatControllerImpl { }), in: .current) } - func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { + func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, sourceView: UIView? = nil, completion: @escaping (ContextMenuActionResult) -> Void) { let _ = (self.context.engine.data.get( EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) ) @@ -564,7 +565,17 @@ extension ChatControllerImpl { isChannel = true } + var contextItems: [ContextMenuItem] = [] + var canDisplayContextMenu = true + if options.contains(.cancelSending) { + contextItems.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCancelSending, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in + f(.dismissWithoutContent) + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + strongSelf.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) + } + }))) items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { @@ -574,9 +585,6 @@ extension ChatControllerImpl { })) } - var contextItems: [ContextMenuItem] = [] - var canDisplayContextMenu = true - var unsendPersonalMessages = false if options.contains(.unsendPersonal) { canDisplayContextMenu = false @@ -705,6 +713,16 @@ extension ChatControllerImpl { if canDisplayContextMenu, let contextController = contextController { contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) + } else if canDisplayContextMenu, let sourceView = sourceView { + let contextController = makeContextController( + presentationData: self.presentationData, + source: .reference(ChatControllerContextReferenceContentSource(controller: self, sourceView: sourceView, insets: .zero, actionsOnTop: true)), + items: .single(ContextController.Items(content: .list(contextItems))), + gesture: nil + ) + self.chatDisplayNode.dismissInput() + self.presentInGlobalOverlay(contextController) + completion(.default) } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in