From 3825ddd778a1d2f85007d437ecbc0d3917f9f157 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 00:09:59 +0400 Subject: [PATCH] Folder improvements --- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListController.swift | 42 +++-- .../Sources/ChatListControllerNode.swift | 152 +++++++++++++++- .../ChatListFilterPresetController.swift | 71 +++++++- .../Sources/Node/ChatListNode.swift | 40 +++- .../ContextControllerActionsStackNode.swift | 57 ++++-- .../TelegramEngine/Peers/Communities.swift | 2 +- .../Components/ActionPanelComponent/BUILD | 23 +++ .../Sources/ActionPanelComponent.swift | 172 ++++++++++++++++++ 9 files changed, 515 insertions(+), 45 deletions(-) create mode 100644 submodules/TelegramUI/Components/ActionPanelComponent/BUILD create mode 100644 submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index f01637a93e..de48533a50 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -92,6 +92,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen", "//submodules/ItemListUI", "//submodules/QrCodeUI", + "//submodules/TelegramUI/Components/ActionPanelComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 02916252d0..defe04e740 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2769,27 +2769,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let previewScreen = ChatFolderLinkPreviewScreen( - context: self.context, - subject: .linkList(folderId: filterId, initialLinks: links ?? []), - contents: ChatFolderLinkContents( - localFilterId: filterId, title: title, - peers: [], - alreadyMemberPeerIds: Set(), - memberCounts: [:] - ), - completion: nil - ) - self.push(previewScreen) + if links == nil || links?.count == 0 { + openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: false, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in + self?.push(c) + }, presentController: { [weak self] c in + self?.present(c, in: .window(.root)) + }, completed: { + }, linkUpdated: { _ in + }) + } else { + let previewScreen = ChatFolderLinkPreviewScreen( + context: self.context, + subject: .linkList(folderId: filterId, initialLinks: links ?? []), + contents: ChatFolderLinkContents( + localFilterId: filterId, title: title, + peers: [], + alreadyMemberPeerIds: Set(), + memberCounts: [:] + ), + completion: nil + ) + self.push(previewScreen) + } }) - - /*openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in - self?.push(c) - }, presentController: { [weak self] c in - self?.present(c, in: .window(.root)) - }, completed: { - }, linkUpdated: { _ in - })*/ } public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 6e583b2866..b5e43dee22 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -14,6 +14,10 @@ import ContextUI import AnimationCache import MultiAnimationRenderer import TelegramUIPreferences +import ActionPanelComponent +import ComponentDisplayAdapters +import ComponentFlow +import ChatFolderLinkPreviewScreen public enum ChatListContainerNodeFilter: Equatable { case all @@ -316,6 +320,14 @@ private final class ChatListShimmerNode: ASDisplayNode { } private final class ChatListContainerItemNode: ASDisplayNode { + private final class TopPanelItem { + let view = ComponentView() + var size: CGSize? + + init() { + } + } + private let context: AccountContext private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer @@ -332,6 +344,13 @@ private final class ChatListContainerItemNode: ASDisplayNode { private var shimmerNodeOffset: CGFloat = 0.0 let listNode: ChatListNode + private var topPanel: TopPanelItem? + + private var pollFilterUpdatesDisposable: Disposable? + private var chatFilterUpdatesDisposable: Disposable? + + private var chatFolderUpdates: ChatFolderUpdates? + private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat)? init(context: AccountContext, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) { @@ -456,7 +475,40 @@ private final class ChatListContainerItemNode: ASDisplayNode { if let (size, insets, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode { strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition) } + strongSelf.layoutAdditionalPanels(transition: transition) } + + if let filter, case let .filter(id, _, _, data) = filter, data.isShared { + self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start() + self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + var update = false + if let result, result.availableChatsToJoin != 0 { + if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin { + update = true + } + self.chatFolderUpdates = result + } else { + if self.chatFolderUpdates != nil { + self.chatFolderUpdates = nil + update = true + } + } + if update { + if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction) = self.validLayout { + self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }) + } + } + + deinit { + self.pollFilterUpdatesDisposable?.dispose() + self.chatFilterUpdatesDisposable?.dispose() } private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -464,6 +516,32 @@ private final class ChatListContainerItemNode: ASDisplayNode { transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size)) } + private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) { + guard let (size, insets, visualNavigationHeight, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else { + return + } + + let _ = size + let _ = insets + + if let topPanel = self.topPanel, let topPanelSize = topPanel.size { + let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height + + if let topPanelView = topPanel.view.view { + var animateIn = false + var topPanelTransition = transition + if topPanelView.bounds.isEmpty { + topPanelTransition = .immediate + animateIn = true + } + topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize)) + if animateIn { + transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height)) + } + } + } + } + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData @@ -479,17 +557,85 @@ private final class ChatListContainerItemNode: ASDisplayNode { func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction) + var listInsets = insets + var additionalTopInset: CGFloat = 0.0 + + if let chatFolderUpdates = self.chatFolderUpdates { + let topPanel: TopPanelItem + var topPanelTransition = Transition(transition) + if let current = self.topPanel { + topPanel = current + } else { + topPanelTransition = .immediate + topPanel = TopPanelItem() + self.topPanel = topPanel + } + + //TODO:localize + let title: String + if chatFolderUpdates.availableChatsToJoin == 1 { + title = "1 New Chat Available" + } else { + title = "\(chatFolderUpdates.availableChatsToJoin) New Chats Available" + } + + let topPanelHeight: CGFloat = 44.0 + + let _ = topPanel.view.update( + transition: topPanelTransition, + component: AnyComponent(ActionPanelComponent( + theme: self.presentationData.theme, + title: title, + action: { [weak self] in + guard let self, let chatFolderUpdates = self.chatFolderUpdates else { + return + } + + self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents)) + }, + dismissAction: { [weak self] in + guard let self, let chatFolderUpdates = self.chatFolderUpdates else { + return + } + let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start() + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: topPanelHeight) + ) + if let topPanelView = topPanel.view.view { + if topPanelView.superview == nil { + self.view.addSubview(topPanelView) + } + } + + topPanel.size = CGSize(width: size.width, height: topPanelHeight) + listInsets.top += topPanelHeight + additionalTopInset += topPanelHeight + } else { + if let topPanel = self.topPanel { + self.topPanel = nil + if let topPanelView = topPanel.view.view { + transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in + topPanelView?.removeFromSuperview() + }) + } + } + } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) - self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight, originalTopInset: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction) + self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction) if let emptyNode = self.emptyNode { - let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) + let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: listInsets.top), size: CGSize(width: size.width, height: size.height - listInsets.top - listInsets.bottom)) transition.updateFrame(node: emptyNode, frame: emptyNodeFrame) emptyNode.updateLayout(size: emptyNodeFrame.size, transition: transition) } + + self.layoutAdditionalPanels(transition: transition) } } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 8e1856cae5..f74fa25561 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -42,6 +42,7 @@ private final class ChatListFilterPresetControllerArguments { let openLink: (ExportedChatFolderLink) -> Void let removeLink: (ExportedChatFolderLink) -> Void let linkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void + let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void init( context: AccountContext, @@ -59,7 +60,8 @@ private final class ChatListFilterPresetControllerArguments { createLink: @escaping () -> Void, openLink: @escaping (ExportedChatFolderLink) -> Void, removeLink: @escaping (ExportedChatFolderLink) -> Void, - linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void + linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void, + peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void ) { self.context = context self.updateState = updateState @@ -77,6 +79,7 @@ private final class ChatListFilterPresetControllerArguments { self.openLink = openLink self.removeLink = removeLink self.linkContextAction = linkContextAction + self.peerContextAction = peerContextAction } } @@ -514,6 +517,12 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) }) }, removePeer: { id in arguments.deleteIncludePeer(id) + }, contextAction: { sourceNode, gesture in + guard let peer = peer.peer else { + gesture?.cancel() + return + } + arguments.peerContextAction(peer, sourceNode, gesture, nil) }) case let .excludePeer(_, peer, isRevealed): return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.chatMainPeer!, height: .peerList, aliasHandling: .threatSelfAsSaved, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: isRevealed), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { @@ -522,6 +531,12 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) }) }, removePeer: { id in arguments.deleteExcludePeer(id) + }, contextAction: { sourceNode, gesture in + guard let peer = peer.peer else { + gesture?.cancel() + return + } + arguments.peerContextAction(peer, sourceNode, gesture, nil) }) case let .includeExpand(text): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: { @@ -1472,6 +1487,32 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) + }, + peerContextAction: { peer, node, gesture, location in + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { _, f in + f(.dismissWithoutContent) + + updateState { state in + var state = state + if let index = state.additionallyExcludePeers.firstIndex(of: peer.id) { + state.additionallyExcludePeers.remove(at: index) + } + if let index = state.additionallyIncludePeers.firstIndex(of: peer.id) { + state.additionallyIncludePeers.remove(at: index) + } + return state + } + }))) + + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + presentInGlobalOverlayImpl?(contextController) } ) @@ -1798,3 +1839,31 @@ private final class InviteLinkContextReferenceContentSource: ContextReferenceCon return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool = true + + init(controller: ViewController, sourceNode: ASDisplayNode?) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceNode = self.sourceNode + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceNode = sourceNode { + return (sourceNode.view, sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index d40a0900a2..82ae5cd402 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1669,6 +1669,30 @@ public final class ChatListNode: ListView { let currentPeerId: EnginePeer.Id = context.account.peerId + + /*let emptyInitialView = ChatListNodeView( + originalList: EngineChatList( + items: [], + groupItems: [], + additionalItems: [], + hasEarlier: false, + hasLater: false, + isLoading: false + ), + filteredEntries: [ChatListNodeEntry.HeaderEntry], + isLoading: false, + filter: nil + ) + let _ = previousView.swap(emptyInitialView) + + let _ = (preparedChatListNodeViewTransition(from: nil, to: emptyInitialView, reason: .initial, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: nil, searchMode: false) + |> map { mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: nil, mode: mode, isPeerEnabled: nil, transition: $0) }).start(next: { [weak self] value in + guard let self else { + return + } + let _ = self.enqueueTransition(value).start() + })*/ + let chatListNodeViewTransition = combineLatest( queue: viewProcessingQueue, hideArchivedFolderByDefault, @@ -1949,7 +1973,7 @@ public final class ChatListNode: ListView { } } if isEmpty { - entries = [] + entries = [.HeaderEntry] } let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter) @@ -1964,6 +1988,8 @@ public final class ChatListNode: ListView { if previous.filteredEntries.count == 1 { if case .HoleEntry = previous.filteredEntries[0] { previousWasEmptyOrSingleHole = true + } else if case .HeaderEntry = previous.filteredEntries[0] { + previousWasEmptyOrSingleHole = true } } else if previous.filteredEntries.isEmpty && previous.isLoading { previousWasEmptyOrSingleHole = true @@ -2655,7 +2681,9 @@ public final class ChatListNode: ListView { } private func pollFilterUpdates() { - guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else { + self.chatFolderUpdates.set(.single(nil)) + + /*guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else { self.chatFolderUpdates.set(.single(nil)) return } @@ -2666,7 +2694,7 @@ public final class ChatListNode: ListView { return } self.chatFolderUpdates.set(.single(result)) - }) + })*/ } private func resetFilter() { @@ -2960,7 +2988,8 @@ public final class ChatListNode: ListView { scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } - self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatListOpaqueTransactionState(chatListView: transition.chatListView), completion: completion) + let updatedOpaqueState: Any? = ChatListOpaqueTransactionState(chatListView: transition.chatListView) + self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: updatedOpaqueState, completion: completion) } } @@ -2968,6 +2997,8 @@ public final class ChatListNode: ListView { switch self.visibleContentOffset() { case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0: return false + case .none: + return false default: return true } @@ -3070,6 +3101,7 @@ public final class ChatListNode: ListView { if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true + self.dequeueTransition() } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index dba9b76e24..d50dd29b60 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -311,24 +311,49 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin if let currentBadge = self.currentBadge, currentBadge.badge == badge { badgeImage = currentBadge.image } else { - let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor - let badgeString = NSAttributedString(string: badge.value, font: Font.semibold(11.0), textColor: badgeTextColor) - let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) - let badgeSideInset: CGFloat = 3.0 - let badgeVerticalInset: CGFloat = 1.0 - let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height)) - badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath) - context.fillPath() + switch badge.style { + case .badge: + let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor + let badgeString = NSAttributedString(string: badge.value, font: Font.regular(13.0), textColor: badgeTextColor) + let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) - UIGraphicsPushContext(context) + let badgeSideInset: CGFloat = 5.0 + let badgeVerticalInset: CGFloat = 1.0 + var badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height)) + badgeBackgroundSize.width = max(badgeBackgroundSize.width, badgeBackgroundSize.height) + badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath) + context.fillPath() + + UIGraphicsPushContext(context) + + badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + floor((badgeBackgroundSize.width - badgeTextBounds.width) * 0.5), y: badgeTextBounds.minY + badgeVerticalInset)) + + UIGraphicsPopContext() + }) + case .label: + let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor + let badgeString = NSAttributedString(string: badge.value, font: Font.semibold(11.0), textColor: badgeTextColor) + let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) - badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + badgeSideInset + UIScreenPixel, y: badgeTextBounds.minY + badgeVerticalInset + UIScreenPixel)) - - UIGraphicsPopContext() - }) + let badgeSideInset: CGFloat = 3.0 + let badgeVerticalInset: CGFloat = 1.0 + let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height)) + badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath) + context.fillPath() + + UIGraphicsPushContext(context) + + badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + badgeSideInset + UIScreenPixel, y: badgeTextBounds.minY + badgeVerticalInset + UIScreenPixel)) + + UIGraphicsPopContext() + }) + } } let badgeIconNode: ASImageNode diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index cc8c0f6dd7..9298cd5e40 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -504,7 +504,7 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi } public final class ChatFolderUpdates: Equatable { - fileprivate let folderId: Int32 + public let folderId: Int32 fileprivate let title: String fileprivate let missingPeers: [EnginePeer] fileprivate let memberCounts: [EnginePeer.Id: Int] diff --git a/submodules/TelegramUI/Components/ActionPanelComponent/BUILD b/submodules/TelegramUI/Components/ActionPanelComponent/BUILD new file mode 100644 index 0000000000..e7bc81265a --- /dev/null +++ b/submodules/TelegramUI/Components/ActionPanelComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ActionPanelComponent", + module_name = "ActionPanelComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/AnimatedCountLabelNode", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift b/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift new file mode 100644 index 0000000000..e827d85d98 --- /dev/null +++ b/submodules/TelegramUI/Components/ActionPanelComponent/Sources/ActionPanelComponent.swift @@ -0,0 +1,172 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AppBundle + +public final class ActionPanelComponent: Component { + public let theme: PresentationTheme + public let title: String + public let action: () -> Void + public let dismissAction: () -> Void + + public init( + theme: PresentationTheme, + title: String, + action: @escaping () -> Void, + dismissAction: @escaping () -> Void + ) { + self.theme = theme + self.title = title + self.action = action + self.dismissAction = dismissAction + } + + public static func ==(lhs: ActionPanelComponent, rhs: ActionPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private let backgroundView: BlurredBackgroundView + private let separatorLayer: SimpleLayer + + private let contentView: UIView + private let title = ComponentView() + + private let dismissButton: HighlightTrackingButton + private let dismissIconView: UIImageView + + private var component: ActionPanelComponent? + + public override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.backgroundView.isUserInteractionEnabled = false + + self.separatorLayer = SimpleLayer() + self.contentView = UIView() + self.contentView.isUserInteractionEnabled = false + + self.dismissButton = HighlightTrackingButton() + self.dismissIconView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.contentView) + + self.dismissButton.addSubview(self.dismissIconView) + self.addSubview(self.dismissButton) + + self.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.contentView.layer.removeAnimation(forKey: "opacity") + self.contentView.alpha = 0.65 + } else { + self.contentView.alpha = 1.0 + self.contentView.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.dismissButton.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.dismissButton.layer.removeAnimation(forKey: "opacity") + self.dismissButton.alpha = 0.65 + } else { + self.dismissButton.alpha = 1.0 + self.dismissButton.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) + } + } + } + self.dismissButton.addTarget(self, action: #selector(self.dismissPressed), for: .touchUpInside) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + @objc private func dismissPressed() { + guard let component = self.component else { + return + } + component.dismissAction() + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + func update(component: ActionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + + self.dismissIconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/EncircledCloseButton")?.withRenderingMode(.alwaysTemplate) + self.dismissIconView.tintColor = component.theme.rootController.navigationBar.accentTextColor + } + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let rightInset: CGFloat = 44.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.theme.rootController.navigationBar.accentTextColor)), + environment: {}, + containerSize: CGSize(width: availableSize.width - rightInset, height: availableSize.height) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + self.contentView.addSubview(titleView) + } + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - titleSize.height) * 0.5)), size: titleSize) + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + let dismissButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset, y: 0.0), size: CGSize(width: rightInset, height: availableSize.height)) + transition.setFrame(view: self.dismissButton, frame: dismissButtonFrame) + if let iconImage = self.dismissIconView.image { + transition.setFrame(view: self.dismissIconView, frame: CGRect(origin: CGPoint(x: floor((dismissButtonFrame.width - iconImage.size.width) * 0.5), y: floor((dismissButtonFrame.height - iconImage.size.height) * 0.5)), size: iconImage.size)) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +}