Various improvements

This commit is contained in:
Ilya Laktyushin 2026-06-04 01:44:36 +02:00
parent 11a4dbf0a0
commit ec8bd9acdd
24 changed files with 570 additions and 371 deletions

View file

@ -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 %@?";

View file

@ -1125,7 +1125,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ in
}, cancelMessageSelection: { _ in
}, deleteSelectedMessages: {
}, deleteSelectedMessages: { _ in
}, reportSelectedMessages: {
}, reportMessages: { _, _ in
}, blockMessageAuthor: { _, _ in

View file

@ -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",

View file

@ -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))
}
})
}

View file

@ -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

View file

@ -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<EngineMessage.Id>?, 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<EngineMessage.Id>?, 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<EngineMessage.Id>?
@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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",

View file

@ -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))
}
}
}
}

View file

@ -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<Empty>()
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() {

View file

@ -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<Void>()
fileprivate let _title = Promise<String>()
fileprivate let _titleContent = Promise<GalleryTitleView.Content?>(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<String, NoError> {
return self._title.get()
}
override func titleContent() -> Signal<GalleryTitleView.Content?, NoError> {
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))
}

View file

@ -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

View file

@ -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() {

View file

@ -58,7 +58,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ in
}, cancelMessageSelection: { _ in
}, deleteSelectedMessages: {
}, deleteSelectedMessages: { _ in
}, reportSelectedMessages: {
}, reportMessages: { _, _ in
}, blockMessageAuthor: { _, _ in

View file

@ -407,7 +407,7 @@ public final class ChatTextInputPanelComponent: Component {
},
cancelMessageSelection: { _ in
},
deleteSelectedMessages: {
deleteSelectedMessages: { _ in
},
reportSelectedMessages: {
},

View file

@ -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

View file

@ -47,7 +47,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ in
}, cancelMessageSelection: { _ in
}, deleteSelectedMessages: {
}, deleteSelectedMessages: { _ in
deleteMessages()
}, reportSelectedMessages: {
reportMessages()

View file

@ -387,7 +387,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ in
}, cancelMessageSelection: { _ in
}, deleteSelectedMessages: {
}, deleteSelectedMessages: { _ in
}, reportSelectedMessages: {
}, reportMessages: { _, _ in
}, blockMessageAuthor: { _, _ in

View file

@ -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 })
}
}
}

View file

@ -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)
}
}

View file

@ -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<EngineMessage.Id>, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) {
func presentDeleteMessageOptions(messageIds: Set<EngineMessage.Id>, 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