From 3825ddd778a1d2f85007d437ecbc0d3917f9f157 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 00:09:59 +0400 Subject: [PATCH 1/9] 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) + } +} From 42f43bf767029b6d66a642503791dee91bee89e8 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 15:31:56 +0400 Subject: [PATCH 2/9] Folder improvements --- submodules/CheckNode/Sources/CheckNode.swift | 310 ++++++++---------- .../Display/Source/CAAnimationUtils.swift | 4 + .../Sources/PremiumLimitScreen.swift | 22 +- .../Sources/Network/FetchV2.swift | 9 + .../Components/AnimatedCounterComponent/BUILD | 19 ++ .../Sources/AnimatedCounterComponent.swift | 279 ++++++++++++++++ .../ChatFolderLinkPreviewScreen/BUILD | 2 + .../Sources/ChatFolderLinkPreviewScreen.swift | 41 +-- .../Components/PlainButtonComponent/BUILD | 19 ++ .../Sources/PlainButtonComponent.swift | 154 +++++++++ 10 files changed, 664 insertions(+), 195 deletions(-) create mode 100644 submodules/TelegramUI/Components/AnimatedCounterComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift create mode 100644 submodules/TelegramUI/Components/PlainButtonComponent/BUILD create mode 100644 submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 2776218960..03259a5ed9 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -127,6 +127,28 @@ public class CheckNode: ASDisplayNode { animation.timingFunction = CAMediaTimingFunction(name: selected ? CAMediaTimingFunctionName.easeOut : CAMediaTimingFunctionName.easeIn) animation.duration = selected ? 0.21 : 0.15 self.pop_add(animation, forKey: "progress") + + if selected { + self.layer.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + self.layer.animateScale(from: 0.9, to: 1.1, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: 1.1, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + }) + } else { + self.layer.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + self.layer.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + }) + } } else { self.pop_removeAllAnimations() self.animatingOut = false @@ -152,108 +174,11 @@ public class CheckNode: ASDisplayNode { } if let parameters = parameters as? CheckNodeParameters { - let center = CGPoint(x: bounds.width / 2.0, y: bounds.width / 2.0) - - var borderWidth: CGFloat = 1.0 + UIScreenPixel - if parameters.theme.hasInset { - borderWidth = 1.5 - } - if let customBorderWidth = parameters.theme.borderWidth { - borderWidth = customBorderWidth - } - - let checkWidth: CGFloat = 1.5 - - let inset: CGFloat = parameters.theme.hasInset ? 2.0 - UIScreenPixel : 0.0 - - let checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress - let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35) - - context.setStrokeColor(parameters.theme.borderColor.cgColor) - if parameters.theme.isDottedBorder { - context.setLineDash(phase: 0.0, lengths: [4.0, 4.0]) - } - context.setLineWidth(borderWidth) - - let maybeScaleOut = { - let animate: Bool - if case .counter = parameters.content { - animate = true - } else if parameters.animatingOut { - animate = true - } else { - animate = false - } - if animate { - context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0) - context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress) - context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0) - - context.setAlpha(parameters.animationProgress) - } - } - - let borderInset = borderWidth / 2.0 + inset - let borderProgress: CGFloat = parameters.theme.filledBorder ? fillProgress : 1.0 - let borderFrame = bounds.insetBy(dx: borderInset, dy: borderInset) - - if parameters.theme.filledBorder { - maybeScaleOut() - } - - context.saveGState() - if parameters.theme.hasShadow { - context.setShadow(offset: CGSize(), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) - } - - context.strokeEllipse(in: borderFrame.insetBy(dx: borderFrame.width * (1.0 - borderProgress), dy: borderFrame.height * (1.0 - borderProgress))) - context.restoreGState() - - if !parameters.theme.filledBorder { - maybeScaleOut() - } - - context.setFillColor(parameters.theme.backgroundColor.cgColor) - - let fillInset = parameters.theme.overlayBorder ? borderWidth + inset : inset - let fillFrame = bounds.insetBy(dx: fillInset, dy: fillInset) - context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress))) - - switch parameters.content { - case .check: - let scale = (bounds.width - inset) / 18.0 - let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0)) - let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale) - let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale) - let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale) - - if !firstSegment.isZero { - if firstSegment < 1.0 { - context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) - context.addLine(to: s) - } else { - let secondSegment = (checkProgress - 0.33) * 1.5 - context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) - context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) - context.addLine(to: s) - } - } - - context.setStrokeColor(parameters.theme.strokeColor.cgColor) - if parameters.theme.strokeColor == .clear { - context.setBlendMode(.clear) - } - context.setLineWidth(checkWidth) - context.setLineCap(.round) - context.setLineJoin(.round) - context.setMiterLimit(10.0) - - context.strokePath() - case let .counter(number): - let string = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .semibold), textColor: parameters.theme.strokeColor) - let stringSize = string.boundingRect(with: bounds.size, options: .usesLineFragmentOrigin, context: nil).size - string.draw(at: CGPoint(x: floorToScreenPixels((bounds.width - stringSize.width) / 2.0), y: floorToScreenPixels((bounds.height - stringSize.height) / 2.0))) - } + CheckLayer.drawContents( + context: context, + size: bounds.size, + parameters: parameters + ) } } @@ -388,6 +313,28 @@ public class CheckLayer: CALayer { animation.timingFunction = CAMediaTimingFunction(name: selected ? CAMediaTimingFunctionName.easeOut : CAMediaTimingFunctionName.easeIn) animation.duration = selected ? 0.21 : 0.15 self.pop_add(animation, forKey: "progress") + + if selected { + self.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + self.animateScale(from: 0.9, to: 1.1, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.animateScale(from: 1.1, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + }) + } else { + self.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + self.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + }) + } } else { self.pop_removeAllAnimations() self.animatingOut = false @@ -404,100 +351,125 @@ public class CheckLayer: CALayer { return } self.contents = generateImage(self.bounds.size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) + CheckLayer.drawContents( + context: context, + size: size, + parameters: CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut) + ) + })?.cgImage + } + + fileprivate static func drawContents(context: CGContext, size: CGSize, parameters: CheckNodeParameters) { + context.clear(CGRect(origin: CGPoint(), size: size)) - let parameters = CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut) + let center = CGPoint(x: size.width / 2.0, y: size.width / 2.0) - let center = CGPoint(x: bounds.width / 2.0, y: bounds.width / 2.0) + var borderWidth: CGFloat = 1.0 + UIScreenPixel + if parameters.theme.hasInset { + borderWidth = 1.5 + } + if let customBorderWidth = parameters.theme.borderWidth { + borderWidth = customBorderWidth + } - var borderWidth: CGFloat = 1.0 + UIScreenPixel - if parameters.theme.hasInset { - borderWidth = 1.5 - } - if let customBorderWidth = parameters.theme.borderWidth { - borderWidth = customBorderWidth + let checkWidth: CGFloat = 1.5 + + let inset: CGFloat = parameters.theme.hasInset ? 2.0 - UIScreenPixel : 0.0 + + let checkProgress: CGFloat + + context.setStrokeColor(parameters.theme.borderColor.cgColor) + context.setLineWidth(borderWidth) + + let maybeScaleOut = { + if parameters.animatingOut { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setAlpha(parameters.animationProgress) } + } - let checkWidth: CGFloat = 1.5 - - let inset: CGFloat = parameters.theme.hasInset ? 2.0 - UIScreenPixel : 0.0 - - let checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress + if !parameters.theme.filledBorder { + checkProgress = parameters.animationProgress + + let fillProgress: CGFloat = parameters.animationProgress + + context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0) + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter))) + context.setBlendMode(.normal) + } else { + checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress + let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35) - - context.setStrokeColor(parameters.theme.borderColor.cgColor) - context.setLineWidth(borderWidth) - - let maybeScaleOut = { - if parameters.animatingOut { - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - context.setAlpha(parameters.animationProgress) - } - } - + let borderInset = borderWidth / 2.0 + inset let borderProgress: CGFloat = parameters.theme.filledBorder ? fillProgress : 1.0 - let borderFrame = bounds.insetBy(dx: borderInset, dy: borderInset) - + let borderFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: borderInset, dy: borderInset) + if parameters.theme.filledBorder { maybeScaleOut() } - + context.saveGState() if parameters.theme.hasShadow { context.setShadow(offset: CGSize(), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) } - + context.strokeEllipse(in: borderFrame.insetBy(dx: borderFrame.width * (1.0 - borderProgress), dy: borderFrame.height * (1.0 - borderProgress))) context.restoreGState() - + if !parameters.theme.filledBorder { maybeScaleOut() } - + context.setFillColor(parameters.theme.backgroundColor.cgColor) - + let fillInset = parameters.theme.overlayBorder ? borderWidth + inset : inset - let fillFrame = bounds.insetBy(dx: fillInset, dy: fillInset) + let fillFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: fillInset, dy: fillInset) context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress))) + } - switch parameters.content { - case .check: - let scale = (bounds.width - inset) / 18.0 - let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0)) - let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale) - let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale) - let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale) + switch parameters.content { + case .check: + let scale = (size.width - inset) / 18.0 + let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0)) + let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale) + let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale) + let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale) - if !firstSegment.isZero { - if firstSegment < 1.0 { - context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) - context.addLine(to: s) - } else { - let secondSegment = (checkProgress - 0.33) * 1.5 - context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) - context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) - context.addLine(to: s) - } + if !firstSegment.isZero { + if firstSegment < 1.0 { + context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) + context.addLine(to: s) + } else { + let secondSegment = (checkProgress - 0.33) * 1.5 + context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) + context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) + context.addLine(to: s) } + } - context.setStrokeColor(parameters.theme.strokeColor.cgColor) - if parameters.theme.strokeColor == .clear { - context.setBlendMode(.clear) - } - context.setLineWidth(checkWidth) - context.setLineCap(.round) - context.setLineJoin(.round) - context.setMiterLimit(10.0) + context.setStrokeColor(parameters.theme.strokeColor.cgColor) + if parameters.theme.strokeColor == .clear { + context.setBlendMode(.clear) + } + context.setLineWidth(checkWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setMiterLimit(10.0) - context.strokePath() - case let .counter(number): - let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor) - text.draw(at: CGPoint()) - } - })?.cgImage + context.strokePath() + case let .counter(number): + let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor) + text.draw(at: CGPoint()) + } } } diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 698abec787..ac53fb66ca 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -365,6 +365,10 @@ public extension CALayer { func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) } + + func animateSublayerScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "sublayerTransform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } func animateScaleX(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 6eff33fb93..58ff0a2a17 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -770,8 +770,12 @@ private final class LimitSheetContent: CombinedComponent { string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) - badgeGraphPosition = badgePosition + if component.count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) if !state.isPremium && badgePosition > 0.5 { string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string @@ -811,11 +815,11 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = count > limit ? "\(limit)" : "" premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)" if count >= premiumLimit { - badgeGraphPosition = max(0.1, CGFloat(limit) / CGFloat(premiumLimit)) + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) } else { - badgeGraphPosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) + badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit)) } - badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) + badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit)) if isPremiumDisabled { badgeText = "\(limit)" @@ -829,8 +833,12 @@ private final class LimitSheetContent: CombinedComponent { string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) - badgeGraphPosition = badgePosition + if component.count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) if isPremiumDisabled { badgeText = "\(limit)" diff --git a/submodules/TelegramCore/Sources/Network/FetchV2.swift b/submodules/TelegramCore/Sources/Network/FetchV2.swift index 37136a08d9..e4a05c99f8 100644 --- a/submodules/TelegramCore/Sources/Network/FetchV2.swift +++ b/submodules/TelegramCore/Sources/Network/FetchV2.swift @@ -641,10 +641,13 @@ private final class FetchImpl { isComplete = true let resultingSize = fetchRange.lowerBound + actualLength if let currentKnownSize = self.knownSize { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to min(\(currentKnownSize), \(resultingSize)) = \(min(currentKnownSize, resultingSize))") self.knownSize = min(currentKnownSize, resultingSize) } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to \(resultingSize)") self.knownSize = resultingSize } + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): reporting resource size \(fetchRange.lowerBound + actualLength)") self.onNext(.resourceSizeUpdated(fetchRange.lowerBound + actualLength)) } @@ -662,15 +665,21 @@ private final class FetchImpl { } else { actualData = Data() } + + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): extracting aligned part \(partRange) (\(fetchRange)): \(actualData.count)") } if !actualData.isEmpty { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): emitting data part \(partRange) (aligned as \(fetchRange)): \(actualData.count), isComplete: \(isComplete)") + self.onNext(.dataPart( resourceOffset: partRange.lowerBound, data: actualData, range: 0 ..< Int64(actualData.count), complete: isComplete )) + } else { + Logger.shared.log("FetchV2", "\(self.loggingIdentifier): not emitting data part \(partRange) (aligned as \(fetchRange))") } case let .cdnRedirect(cdnData): self.state = .fetching(FetchImpl.FetchingState( diff --git a/submodules/TelegramUI/Components/AnimatedCounterComponent/BUILD b/submodules/TelegramUI/Components/AnimatedCounterComponent/BUILD new file mode 100644 index 0000000000..d62d5882a6 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimatedCounterComponent/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AnimatedCounterComponent", + module_name = "AnimatedCounterComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift b/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift new file mode 100644 index 0000000000..8f77f6976a --- /dev/null +++ b/submodules/TelegramUI/Components/AnimatedCounterComponent/Sources/AnimatedCounterComponent.swift @@ -0,0 +1,279 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +final class AnimatedCounterItemComponent: Component { + public let font: UIFont + public let color: UIColor + public let text: String + public let numericValue: Int + public let alignment: CGFloat + + public init( + font: UIFont, + color: UIColor, + text: String, + numericValue: Int, + alignment: CGFloat + ) { + self.font = font + self.color = color + self.text = text + self.numericValue = numericValue + self.alignment = alignment + } + + public static func ==(lhs: AnimatedCounterItemComponent, rhs: AnimatedCounterItemComponent) -> Bool { + if lhs.font != rhs.font { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.numericValue != rhs.numericValue { + return false + } + if lhs.alignment != rhs.alignment { + return false + } + return true + } + + public final class View: UIView { + private let contentView: UIImageView + + private var component: AnimatedCounterItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.contentView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.contentView) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: AnimatedCounterItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousNumericValue = self.component?.numericValue + + self.component = component + self.state = state + + let text = NSAttributedString(string: component.text, font: component.font, textColor: component.color) + let textBounds = text.boundingRect(with: availableSize, options: [.usesLineFragmentOrigin], context: nil) + let size = CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height)) + + let previousContentImage = self.contentView.image + let previousContentFrame = self.contentView.frame + + self.contentView.image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + + text.draw(at: textBounds.origin) + + UIGraphicsPopContext() + }) + self.contentView.frame = CGRect(origin: CGPoint(), size: size) + + if !transition.animation.isImmediate, let previousContentImage, !previousContentFrame.isEmpty, let previousNumericValue, previousNumericValue != component.numericValue { + let previousContentView = UIImageView() + previousContentView.image = previousContentImage + previousContentView.frame = CGRect(origin: CGPoint(x: size.width * component.alignment - previousContentFrame.width * component.alignment, y: previousContentFrame.minY), size: previousContentFrame.size) + self.addSubview(previousContentView) + + let offsetY: CGFloat = size.height * 0.6 * (previousNumericValue < component.numericValue ? -1.0 : 1.0) + + let subTransition = Transition(animation: .curve(duration: 0.16, curve: .easeInOut)) + + subTransition.animatePosition(view: self.contentView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + subTransition.animateAlpha(view: self.contentView, from: 0.0, to: 1.0) + + subTransition.setPosition(view: previousContentView, position: CGPoint(x: previousContentView.layer.position.x, y: previousContentView.layer.position.y - offsetY)) + subTransition.setAlpha(view: previousContentView, alpha: 0.0, completion: { [weak previousContentView] _ in + previousContentView?.removeFromSuperview() + }) + } + + return size + } + } + + 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) + } +} + + +public final class AnimatedCounterComponent: Component { + public enum Alignment { + case left + case right + } + + public struct Item: Equatable { + public var id: AnyHashable + public var text: String + public var numericValue: Int + + public init(id: AnyHashable, text: String, numericValue: Int) { + self.id = id + self.text = text + self.numericValue = numericValue + } + } + + public let font: UIFont + public let color: UIColor + public let alignment: Alignment + public let items: [Item] + + public init( + font: UIFont, + color: UIColor, + alignment: Alignment, + items: [Item] + ) { + self.font = font + self.color = color + self.alignment = alignment + self.items = items + } + + public static func ==(lhs: AnimatedCounterComponent, rhs: AnimatedCounterComponent) -> Bool { + if lhs.font != rhs.font { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.alignment != rhs.alignment { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + private final class ItemView { + let view = ComponentView() + } + + public final class View: UIView { + private var itemViews: [AnyHashable: ItemView] = [:] + + private var component: AnimatedCounterComponent? + private weak var state: EmptyComponentState? + + private var measuredSpaceWidth: CGFloat? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: AnimatedCounterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let spaceWidth: CGFloat + if let measuredSpaceWidth = self.measuredSpaceWidth, let previousComponent = self.component, previousComponent.font.pointSize == component.font.pointSize { + spaceWidth = measuredSpaceWidth + } else { + spaceWidth = ceil(NSAttributedString(string: " ", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).width) + self.measuredSpaceWidth = spaceWidth + } + + self.component = component + self.state = state + + var size = CGSize() + + var validIds: [AnyHashable] = [] + for item in component.items { + if size.width != 0.0 { + size.width += spaceWidth + } + + validIds.append(item.id) + + let itemView: ItemView + var itemTransition = transition + if let current = self.itemViews[item.id] { + itemView = current + } else { + itemTransition = .immediate + itemView = ItemView() + self.itemViews[item.id] = itemView + } + + let itemSize = itemView.view.update( + transition: itemTransition, + component: AnyComponent(AnimatedCounterItemComponent( + font: component.font, + color: component.color, + text: item.text, + numericValue: item.numericValue, + alignment: component.alignment == .left ? 0.0 : 1.0 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + if let itemComponentView = itemView.view.view { + if itemComponentView.superview == nil { + self.addSubview(itemComponentView) + } + let itemFrame = CGRect(origin: CGPoint(x: size.width, y: 0.0), size: itemSize) + switch component.alignment { + case .left: + itemComponentView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5) + itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.minX, y: itemFrame.midY)) + case .right: + itemComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5) + itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.maxX, y: itemFrame.midY)) + } + itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + } + + size.width += itemSize.width + size.height = max(size.height, itemSize.height) + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + if let componentView = itemView.view.view { + transition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in + componentView?.removeFromSuperview() + }) + } + } + } + + return size + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 585ce7411f..0adf863528 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -26,6 +26,8 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/Components/SolidRoundedButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/AnimatedCounterComponent", "//submodules/AvatarNode", "//submodules/CheckNode", "//submodules/Markdown", diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 10536d30d5..a3bbd80f3b 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -20,6 +20,8 @@ import ButtonComponent import ContextUI import QrCodeUI import InviteLinksUI +import PlainButtonComponent +import AnimatedCounterComponent private final class ChatFolderLinkPreviewScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -154,7 +156,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.addSubview(self.navigationBarContainer) - self.scrollView.delaysContentTouches = true + self.scrollView.delaysContentTouches = false self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { @@ -800,15 +802,21 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { listHeaderTitle = " " } - let listHeaderActionTitle: String + //TODO:localize + let listHeaderActionItems: [AnimatedCounterComponent.Item] if self.selectedItems.count == self.items.count { - listHeaderActionTitle = "DESELECT ALL" + listHeaderActionItems = [ + AnimatedCounterComponent.Item(id: AnyHashable(0), text: "DESELECT", numericValue: 0), + AnimatedCounterComponent.Item(id: AnyHashable(1), text: "ALL", numericValue: 1) + ] } else { - listHeaderActionTitle = "SELECT ALL" + listHeaderActionItems = [ + AnimatedCounterComponent.Item(id: AnyHashable(0), text: "SELECT", numericValue: 1), + AnimatedCounterComponent.Item(id: AnyHashable(1), text: "ALL", numericValue: 1) + ] } let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor) - let listHeaderActionBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.itemAccentColor) let listHeaderTextSize = self.listHeaderText.update( transition: .immediate, @@ -838,19 +846,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } let listHeaderActionSize = self.listHeaderAction.update( - transition: .immediate, - component: AnyComponent(Button( - content: AnyComponent(MultilineTextComponent( - text: .markdown( - text: listHeaderActionTitle, - attributes: MarkdownAttributes( - body: listHeaderActionBody, - bold: listHeaderActionBody, - link: listHeaderActionBody, - linkAttribute: { _ in nil } - ) - ) + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(AnimatedCounterComponent( + font: Font.regular(13.0), + color: environment.theme.list.itemAccentColor, + alignment: .right, + items: listHeaderActionItems )), + effectAlignment: .right, action: { [weak self] in guard let self, let component = self.component, let linkContents = component.linkContents else { return @@ -877,8 +881,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.scrollContentView.addSubview(listHeaderActionView) } let listHeaderActionFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - listHeaderActionSize.width, y: contentHeight), size: listHeaderActionSize) - contentTransition.setPosition(view: listHeaderActionView, position: CGPoint(x: listHeaderActionFrame.maxX, y: listHeaderActionFrame.minY)) - listHeaderActionView.bounds = CGRect(origin: CGPoint(), size: listHeaderActionFrame.size) + contentTransition.setFrame(view: listHeaderActionView, frame: listHeaderActionFrame) if let linkContents = component.linkContents, !allChatsAdded, linkContents.peers.count > 1 { listHeaderActionView.isHidden = false diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/BUILD b/submodules/TelegramUI/Components/PlainButtonComponent/BUILD new file mode 100644 index 0000000000..146e69465a --- /dev/null +++ b/submodules/TelegramUI/Components/PlainButtonComponent/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PlainButtonComponent", + module_name = "PlainButtonComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift new file mode 100644 index 0000000000..13cfe06639 --- /dev/null +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -0,0 +1,154 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class PlainButtonComponent: Component { + public enum EffectAlignment { + case left + case right + } + + public let content: AnyComponent + public let effectAlignment: EffectAlignment + public let action: () -> Void + + public init( + content: AnyComponent, + effectAlignment: EffectAlignment, + action: @escaping () -> Void + ) { + self.content = content + self.effectAlignment = effectAlignment + self.action = action + } + + public static func ==(lhs: PlainButtonComponent, rhs: PlainButtonComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.effectAlignment != rhs.effectAlignment { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var component: PlainButtonComponent? + private weak var componentState: EmptyComponentState? + + private let contentContainer = UIView() + private let content = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.contentContainer.isUserInteractionEnabled = false + self.addSubview(self.contentContainer) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.contentContainer.layer.removeAnimation(forKey: "opacity") + self.contentContainer.layer.removeAnimation(forKey: "sublayerTransform") + self.contentContainer.alpha = 0.7 + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setScale(layer: self.contentContainer.layer, scale: topScale) + } else { + self.contentContainer.alpha = 1.0 + self.contentContainer.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2) + + let transition = Transition(animation: .none) + transition.setScale(layer: self.contentContainer.layer, scale: 1.0) + + self.contentContainer.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.contentContainer.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result != nil { + return result + } + + if !self.isEnabled { + return nil + } + + if self.bounds.insetBy(dx: -8.0, dy: -8.0).contains(point) { + return self + } + + return nil + } + + func update(component: PlainButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.componentState = state + + self.isEnabled = true + + let contentAlpha: CGFloat = 1.0 + + let contentSize = self.content.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: availableSize + ) + + let size = contentSize + + if let contentView = self.content.view { + var contentTransition = transition + if contentView.superview == nil { + contentTransition = .immediate + contentView.isUserInteractionEnabled = false + self.contentContainer.addSubview(contentView) + } + let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) * 0.5), y: floor((size.height - contentSize.height) * 0.5)), size: contentSize) + + contentTransition.setFrame(view: contentView, frame: contentFrame) + contentTransition.setAlpha(view: contentView, alpha: contentAlpha) + } + + self.contentContainer.layer.anchorPoint = CGPoint(x: component.effectAlignment == .left ? 0.0 : 1.0, y: 0.5) + transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size)) + transition.setPosition(view: self.contentContainer, position: CGPoint(x: component.effectAlignment == .left ? 0.0 : size.width, y: size.height * 0.5)) + + return size + } + } + + 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) + } +} From 515ef8cfc52917ccc7d4298ae9368887ea76ec8f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 16:19:22 +0400 Subject: [PATCH 3/9] Folder improvements --- .../Sources/ChatFolderLinkPreviewScreen.swift | 34 +++++++++++++++-- .../Animations/anim_add_to_folder.json | 1 + .../Sources/UndoOverlayControllerNode.swift | 37 ++++++++++++------- 3 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 submodules/TelegramUI/Resources/Animations/anim_add_to_folder.json diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index a3bbd80f3b..12f07d8485 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -896,7 +896,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) var initialContentHeight = contentHeight - initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5)) + initialContentHeight += min(itemsHeight, floor(singleItemHeight * 3.5)) contentHeight += itemsHeight contentHeight += 24.0 @@ -980,6 +980,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { var chatListController: ChatListController? if let navigationController = controller.navigationController as? NavigationController { for viewController in navigationController.viewControllers.reversed() { + if viewController is ChatFolderLinkPreviewScreen { + continue + } + if let rootController = viewController as? TabBarController { for c in rootController.controllers { if let c = c as? ChatListController { @@ -1083,7 +1087,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { //TODO:localize let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + var isUpdates = false if case .updates = component.subject { + isUpdates = true + } else { + if component.linkContents?.localFilterId != nil { + isUpdates = true + } + } + + if isUpdates { let chatCountString: String if result.newChatCount == 1 { chatCountString = "1 new chat" @@ -1091,7 +1105,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { chatCountString = "\(result.newChatCount) new chats" } - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Updated", text: "You have joined \(chatCountString)", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current) + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: "Folder \(result.title) Updated", text: "You have joined \(chatCountString)", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } else if result.newChatCount != 0 { let chatCountString: String if result.newChatCount == 1 { @@ -1100,9 +1114,21 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { chatCountString = "\(result.newChatCount) chats" } - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Added", text: "You also joined \(chatCountString)", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current) + let animationBackgroundColor: UIColor + if presentationData.theme.overallDarkAppearance { + animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + } else { + animationBackgroundColor = UIColor(rgb: 0x474747) + } + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: "Folder \(result.title) Added", text: "You also joined \(chatCountString)", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } else { - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Folder \(result.title) Added", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current) + let animationBackgroundColor: UIColor + if presentationData.theme.overallDarkAppearance { + animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + } else { + animationBackgroundColor = UIColor(rgb: 0x474747) + } + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: "Folder \(result.title) Added", text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) } }) } diff --git a/submodules/TelegramUI/Resources/Animations/anim_add_to_folder.json b/submodules/TelegramUI/Resources/Animations/anim_add_to_folder.json new file mode 100644 index 0000000000..d6a6f34a0c --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_add_to_folder.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":57,"w":400,"h":350,"nm":"Folder New 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Folder Front","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[57.78,31.05,0],"ix":2},"a":{"a":0,"k":[57.78,31.05,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":14,"s":[{"i":[[0,0],[2.227,-6.498],[0,0],[0,-1.195],[-11.046,0],[0,0],[-2.227,6.498],[0,0],[0,1.195],[11.046,0]],"o":[[-9.176,0],[0,0],[-0.397,1.159],[0,8.063],[0,0],[9.176,0],[0,0],[0.397,-1.159],[0,-8.063],[0,0]],"v":[[-12.096,6.216],[-31.498,17.271],[-48.512,66.908],[-49.11,70.451],[-29.11,85.05],[127.657,85.05],[147.059,73.994],[163.661,26.719],[164.259,23.176],[144.259,8.577]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.4,"y":0},"t":27,"s":[{"i":[[0,0],[5.516,-7.616],[0,0],[0,-1.636],[-11.046,0],[0,0],[-3.702,4.718],[0,0],[-0.287,1.611],[17.899,0.533]],"o":[[-13.886,0.099],[0,0],[-0.397,1.588],[0,11.046],[0,0],[9.176,0],[0,0],[1.817,-2.298],[2.019,-11.354],[0,0]],"v":[[15.169,-0.792],[-12.083,14.287],[-46.038,57.422],[-49.11,65.05],[-29.11,85.05],[127.657,85.05],[147.059,69.904],[179.824,27.398],[186.808,16.342],[164.086,-0.858]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0},"t":41,"s":[{"i":[[0,0],[2.227,-8.902],[0,0],[0,-1.636],[-11.046,0],[0,0],[-2.227,8.902],[0,0],[0,1.636],[11.046,0]],"o":[[-9.176,0],[0,0],[-0.397,1.588],[0,11.046],[0,0],[9.176,0],[0,0],[0.397,-1.588],[0,-11.046],[0,0]],"v":[[-19.91,-30.122],[-39.312,-14.977],[-48.512,60.196],[-49.11,65.05],[-29.11,85.05],[127.657,85.05],[147.059,69.904],[156.259,-5.268],[156.857,-10.122],[136.857,-30.122]],"c":true}]},{"t":57,"s":[{"i":[[0,0],[2.227,-8.902],[0,0],[0,-1.636],[-11.046,0],[0,0],[-2.227,8.902],[0,0],[0,1.636],[11.046,0]],"o":[[-9.176,0],[0,0],[-0.397,1.588],[0,11.046],[0,0],[9.176,0],[0,0],[0.397,-1.588],[0,-11.046],[0,0]],"v":[[-12.096,-22.95],[-31.498,-7.804],[-48.512,60.196],[-49.11,65.05],[-29.11,85.05],[127.657,85.05],[147.059,69.904],[164.072,1.904],[164.671,-2.95],[144.671,-22.95]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Paper","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[-140]},{"t":41,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":5,"s":[63,140,0],"to":[60,-107,0],"ti":[21.5,-66,0]},{"t":41,"s":[196.5,190.5,0]}],"ix":2},"a":{"a":0,"k":[5,-14.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":3,"s":[0,0,100]},{"t":25,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.817},"o":{"x":0.167,"y":0.183},"t":7,"s":[{"i":[[-0.65,-1.421],[0,0],[0.482,-2.258],[0,0],[0,1.563],[0,0],[-4.198,0.087],[-14.973,55.488]],"o":[[0,0],[0.908,1.763],[-11.843,55.532],[-3.639,0],[0,0],[0.536,-1.393],[0,0],[0.406,-1.503]],"v":[[16.939,-25.089],[21.552,-11.162],[22.384,-3.927],[-54.983,22.264],[-59.414,16.933],[-42.822,-9.049],[-38.427,-5.397],[12.23,-25.189]],"c":true}]},{"i":{"x":0.833,"y":0.817},"o":{"x":0.167,"y":0.183},"t":17,"s":[{"i":[[-0.228,-2.877],[0,0],[1.839,-2.538],[0,0],[0.228,2.877],[0,0],[-4.069,0.484],[-29.183,53.209]],"o":[[0,0],[0.379,3.64],[-24.239,33.437],[-3.538,0.28],[0,0],[0.318,-2.605],[0,0],[1.116,-2.034]],"v":[[35.765,-62.31],[46.729,-6.432],[43.267,-0.275],[-48.859,12.248],[-55.678,7.547],[-43.336,-41.567],[-38.388,-45.142],[31.343,-65.757]],"c":true}]},{"i":{"x":0.833,"y":0.817},"o":{"x":0.167,"y":0.183},"t":27,"s":[{"i":[[-0.659,-4.349],[16.896,-32.916],[4.752,-2.294],[0,0],[-0.022,5.209],[1.273,23.429],[-4.286,0.455],[-17.457,21.461]],"o":[[0,0],[-1.699,3.31],[-9.471,15.247],[-6.85,1.014],[0,0],[-0.24,-4.418],[0,0],[3.617,-1.401]],"v":[[97.835,-46.729],[88.2,16.307],[79.81,26.213],[-55.106,58.355],[-66.276,49.585],[-46.927,-9.122],[-44.085,-19.527],[91.651,-50.567]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.183},"t":34,"s":[{"i":[[1.564,-9.117],[0,0],[6.889,-0.844],[0,0],[-1.33,6.325],[0,0],[-7.219,0.656],[0,0]],"o":[[0,0],[-1.623,3.404],[0,0],[-8.918,-0.04],[0,0],[1.783,-3.832],[0,0],[5.358,0]],"v":[[161.863,-8.438],[128.764,51.308],[115.565,62.55],[-44.616,78.002],[-58.265,63.015],[-27.343,5.488],[-11.35,-6.154],[149.641,-22.078]],"c":true}]},{"t":41,"s":[{"i":[[0,-5.355],[0,0],[7.172,-0.931],[0,0],[0,5.355],[0,0],[-7.528,0.723],[0,0]],"o":[[0,0],[-1.755,3.475],[0,0],[-5.515,0],[0,0],[1.928,-3.947],[0,0],[5.515,0]],"v":[[156,-12.514],[140.83,61.269],[126.876,71.495],[-26.81,70.997],[-36.796,61.3],[-23.93,-13.814],[-8.917,-24.04],[146.014,-22.211]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5,-14.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":33,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Folder Far","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":8,"s":[10]},{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":27,"s":[-10]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":43,"s":[5]},{"t":54,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":3,"s":[147.274,211.361,0],"to":[62.976,-21.111,0],"ti":[0,0,0]},{"i":{"x":0.647,"y":1},"o":{"x":0.6,"y":0},"t":24,"s":[197.774,139.611,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.347,"y":1},"o":{"x":0.281,"y":0},"t":40,"s":[249.274,169.361,0],"to":[0,0,0],"ti":[0,0,0]},{"t":52,"s":[234.274,153.861,0]}],"ix":2},"a":{"a":0,"k":[34.274,-21.139,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":2,"s":[40,0,100]},{"t":18,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":13,"s":[{"i":[[0,0],[-11.641,-0.603],[0,0],[-3.346,-2.144],[0,0],[-4.559,-0.236],[0,0],[0.179,-3.451],[0,0],[5.246,0],[0,0],[3.437,-10.77],[0,0]],"o":[[0.4,-7.669],[0,0],[4.559,0.236],[0,0],[3.346,2.144],[0,0],[5.239,0.272],[0,0],[-1.081,2.903],[0,0],[-14.946,0],[0,0],[0,0]],"v":[[-31.343,-23.38],[-9.543,-36.173],[52.203,-34.019],[64.458,-30.329],[77.186,-22.173],[89.441,-18.482],[151.238,-14.104],[159.509,-7.163],[158.328,-2.715],[147.415,-0.44],[-7.364,-2.697],[-36.675,13.091],[-53.574,55.488]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.4,"y":0},"t":30,"s":[{"i":[[0,0],[-11.045,0.001],[0,0],[-3.271,-2.83],[0,0],[-4.326,0],[0,0],[0,-4.971],[0,0],[4.971,0],[0,0],[2.948,-13.852],[0,0]],"o":[[0.002,-11.045],[0,0],[4.326,0],[0,0],[3.271,2.83],[0,0],[4.971,0],[0,0],[0,4.971],[0,0],[-14.162,0],[0,0],[0,0]],"v":[[-64.694,-61.634],[-44.692,-81.634],[23.101,-81.634],[34.879,-77.246],[46.309,-70.961],[58.086,-66.573],[123.293,-66.573],[133.279,-57.406],[132.954,-51.02],[121.043,-44.289],[-25.669,-44.289],[-54.929,-21.027],[-65.353,60.809]],"c":true}]},{"t":42,"s":[{"i":[[0,0],[-11.045,0.001],[0,0],[-3.271,-2.83],[0,0],[-4.326,0],[0,0],[0,-4.971],[0,0],[4.971,0],[0,0],[2.948,-13.852],[0,0]],"o":[[0.002,-11.045],[0,0],[4.326,0],[0,0],[3.271,2.83],[0,0],[4.971,0],[0,0],[0,4.971],[0,0],[-14.162,0],[0,0],[0,0]],"v":[[-64.723,-65.95],[-44.721,-85.95],[23.072,-85.95],[34.85,-81.562],[47.295,-70.794],[59.072,-66.406],[124.279,-66.406],[133.279,-57.406],[133.279,-49.95],[124.279,-40.95],[-22.434,-40.95],[-51.777,-17.195],[-64.73,43.672]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arrow 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.554],"y":[0.744]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[-76]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0.114]},"t":14,"s":[-28]},{"t":30,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":0.499},"o":{"x":0.3,"y":0},"t":0,"s":[41.503,199.029,0],"to":[-0.503,-44.029,0],"ti":[-45.497,2.029,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0.252},"t":15,"s":[119.503,79.029,0],"to":[101.497,-6.029,0],"ti":[0,0,0]},{"t":36,"s":[278.503,205.029,0]}],"ix":2},"a":{"a":0,"k":[-125.497,4.029,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,-0.041],[0,0],[-0.041,0],[0,0],[0,0],[-0.023,-0.023],[-0.048,0.048],[0,0],[0,0],[0.043,0.043],[0,0],[0.033,0],[0,-0.068]],"o":[[0,0],[-0.041,0],[0,0],[0,0.041],[0,0],[0,0],[0,0.033],[0.048,0.048],[0,0],[0,0],[0.033,-0.048],[0,0],[-0.023,-0.023],[-0.068,0],[0,0]],"v":[[-131.34,3.057],[-132.056,3.057],[-132.13,3.131],[-132.13,3.96],[-132.056,4.034],[-131.34,4.034],[-131.339,4.606],[-131.303,4.694],[-131.128,4.694],[-129.947,3.623],[-129.932,3.605],[-129.947,3.448],[-131.128,2.34],[-131.215,2.304],[-131.339,2.428]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":4,"s":[{"i":[[0,0],[0,0],[0,-1.32],[0,0],[-1.175,0],[0,0],[0,0],[-0.665,-0.747],[-1.386,1.555],[0,0],[0,0],[1.234,1.38],[0,0],[0.94,0],[0,-2.199]],"o":[[0,0],[-1.175,0],[0,0],[0,1.32],[0,0],[0,0],[0,1.056],[1.385,1.556],[0,0],[0,0],[0.957,-1.552],[0,0],[-0.665,-0.746],[-1.959,0],[0,0]],"v":[[-144.335,-11.532],[-161.017,-13.498],[-163.145,-11.108],[-163.145,15.681],[-161.017,18.071],[-144.335,20.036],[-144.292,38.503],[-143.254,41.318],[-138.238,41.32],[-104.263,6.735],[-103.848,6.183],[-104.263,1.105],[-138.239,-34.663],[-140.746,-35.828],[-144.292,-31.846]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":8,"s":[{"i":[[0,0],[0,0],[0,-1.265],[0,0],[-1.46,0],[0,0],[0,0],[-0.826,-0.716],[-1.721,1.49],[0,0],[0,0],[1.532,1.322],[0,0],[1.168,0],[0,-2.108]],"o":[[0,0],[-1.46,0],[0,0],[0,1.265],[0,0],[0,0],[0,1.012],[1.72,1.491],[0,0],[0,0],[1.189,-1.488],[0,0],[-0.826,-0.715],[-2.433,0],[0,0]],"v":[[-145.887,-10.908],[-179.424,-13.016],[-182.067,-10.726],[-182.067,14.953],[-179.424,17.243],[-145.887,19.351],[-145.834,37.052],[-144.544,39.75],[-138.314,39.752],[-96.114,6.602],[-95.6,6.073],[-96.115,1.206],[-138.315,-33.08],[-141.429,-34.196],[-145.834,-30.379]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[0,0],[0,-1.483],[0,0],[-1.559,0],[0,0],[0,0],[-0.882,-0.839],[-1.838,1.747],[0,0],[0,0],[1.636,1.55],[0,0],[1.247,0],[0,-2.471]],"o":[[0,0],[-1.559,0],[0,0],[0,1.483],[0,0],[0,0],[0,1.186],[1.837,1.748],[0,0],[0,0],[1.269,-1.744],[0,0],[-0.882,-0.838],[-2.598,0],[0,0]],"v":[[-144.997,-13.394],[-186.36,-13.8],[-189.183,-11.115],[-189.183,18.988],[-186.36,21.673],[-144.997,22.078],[-144.94,42.829],[-143.563,45.992],[-136.91,45.994],[-91.843,7.132],[-91.294,6.512],[-91.844,0.806],[-136.911,-39.386],[-140.236,-40.695],[-144.94,-36.22]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0],[0.27,-1.827],[0,0],[-1.635,-0.301],[0,0],[0,0],[-0.772,-1.204],[-2.245,1.797],[0,0],[0,0],[1.434,2.227],[0,0],[1.308,0.241],[0.45,-3.045]],"o":[[0,0],[-1.635,-0.301],[0,0],[-0.27,1.827],[0,0],[0,0],[-0.216,1.462],[1.608,2.509],[0,0],[0,0],[1.648,-1.904],[0,0],[-0.773,-1.203],[-2.725,-0.502],[0,0]],"v":[[-139.957,-19.364],[-153.524,-22.444],[-156.972,-19.681],[-164.805,17.145],[-162.334,20.999],[-147.423,24.15],[-151.138,49.73],[-150.269,53.894],[-143.293,55.183],[-88.965,16.013],[-88.276,15.355],[-87.815,8.218],[-125.526,-50.094],[-128.775,-52.35],[-134.522,-47.746]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[0,0],[0,0],[1.08,-4.086],[0,0],[-2.412,-0.853],[0,0],[0,0],[-0.789,-1.192],[-2.786,3.065],[-8.056,15.524],[0,0],[1.493,2.181],[0,0],[1.31,0.222],[0.404,-3.05]],"o":[[0,0],[-1.892,4.665],[0,0],[-0.536,2.031],[0,0],[0,0],[0.601,1.66],[1.644,2.484],[21.698,-24.351],[1.126,-2.17],[0.694,-2.227],[-3.546,-4.57],[-0.79,-1.191],[-2.73,-0.462],[0,0]],"v":[[-145.719,-28.204],[-149.077,-20.175],[-154.116,-8.412],[-169.847,24.113],[-166.767,29.218],[-158.568,40.162],[-148.254,58.487],[-146.224,63.024],[-138.24,63.078],[-89.869,3.151],[-88.626,-0.617],[-91.143,-6.841],[-132.219,-50.996],[-136.144,-52.926],[-141.174,-48.516]],"c":true}]},{"t":20,"s":[{"i":[[0,0],[0,0],[2.181,-7.503],[0,0],[-3.602,-1.613],[0,0],[0,0],[-0.848,-1.152],[-3.513,5.044],[0.317,3.77],[0,0],[1.643,2.07],[0,0],[1.32,0.156],[0.251,-3.068]],"o":[[0,0],[-2.147,12.166],[0,0],[-0.879,2.352],[0,0],[0,0],[1.873,1.942],[1.767,2.399],[45.207,-64.911],[-0.18,-2.145],[-0.799,-2.731],[-8.99,-11.325],[-0.849,-1.151],[-2.751,-0.325],[0,0]],"v":[[-173.195,-36.843],[-172.839,-24.281],[-181.275,1.521],[-204.178,31.859],[-200.045,38.754],[-187.652,50.276],[-169.748,68.311],[-165.849,73.352],[-156.354,71.333],[-118.587,-21.06],[-120.379,-27.967],[-123.774,-34.313],[-171.113,-56.088],[-176.106,-57.42],[-179.938,-53.161]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-5","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":20,"st":5,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arrow","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":35,"s":[21.503,179.029,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":45,"s":[98.503,179.029,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.471,"y":1},"o":{"x":0.3,"y":0},"t":51,"s":[70.503,179.029,0],"to":[0,0,0],"ti":[0,0,0]},{"t":57.087890625,"s":[74.503,179.029,0]}],"ix":2},"a":{"a":0,"k":[-125.497,4.029,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":35,"s":[0,60,100]},{"t":45,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":36.105,"s":[{"i":[[0,0],[0,0],[0,-1.657],[0,0],[-1.657,0],[0,0],[0,0],[-0.937,-0.938],[-1.953,1.952],[0,0],[0,0],[1.739,1.732],[0,0],[1.325,0],[0,-2.761]],"o":[[0,0],[-1.657,0],[0,0],[0,1.657],[0,0],[0,0],[0,1.326],[1.952,1.953],[0,0],[0,0],[1.349,-1.949],[0,0],[-0.938,-0.937],[-2.761,0],[0,0]],"v":[[-144.223,-15.381],[-173.23,-15.381],[-176.23,-12.381],[-176.23,21.257],[-173.23,24.257],[-144.223,24.257],[-144.162,47.445],[-142.699,50.979],[-135.628,50.981],[-87.731,7.556],[-87.147,6.863],[-87.732,0.487],[-135.629,-44.424],[-139.163,-45.887],[-144.163,-40.887]],"c":true}]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":46,"s":[{"i":[[0,0],[0,0],[0,-1.657],[0,0],[-1.484,0],[0,0],[0,0],[-0.839,-0.938],[-1.75,1.952],[0,0],[0,0],[1.558,1.732],[0,0],[1.187,0],[0,-2.761]],"o":[[0,0],[-1.484,0],[0,0],[0,1.657],[0,0],[0,0],[0,1.326],[1.748,1.953],[0,0],[0,0],[1.208,-1.949],[0,0],[-0.84,-0.937],[-2.473,0],[0,0]],"v":[[-138.012,-16.631],[-153.694,-16.631],[-156.381,-13.631],[-156.381,23.507],[-153.694,26.507],[-138.012,26.507],[-138.118,54.195],[-136.807,57.729],[-130.473,57.731],[-90.03,7.669],[-89.507,6.976],[-90.03,0.6],[-130.474,-48.924],[-133.64,-50.387],[-138.118,-45.387]],"c":true}]},{"t":53,"s":[{"i":[[0,0],[0,0],[0,-1.657],[0,0],[-1.657,0],[0,0],[0,0],[-0.937,-0.938],[-1.953,1.952],[0,0],[0,0],[1.739,1.732],[0,0],[1.325,0],[0,-2.761]],"o":[[0,0],[-1.657,0],[0,0],[0,1.657],[0,0],[0,0],[0,1.326],[1.952,1.953],[0,0],[0,0],[1.349,-1.949],[0,0],[-0.938,-0.937],[-2.761,0],[0,0]],"v":[[-144.223,-15.381],[-161.73,-15.381],[-164.73,-12.381],[-164.73,21.257],[-161.73,24.257],[-144.223,24.257],[-144.223,51.945],[-142.76,55.479],[-135.689,55.481],[-87.731,7.556],[-87.147,6.863],[-87.732,0.487],[-135.689,-47.424],[-139.223,-48.887],[-144.223,-43.887]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-5","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":5,"bm":0}],"markers":[{"tm":60,"cm":"1","dr":0}]} \ No newline at end of file diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 94bdb4ae57..0d79b33c52 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -861,20 +861,31 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animationNode = AnimationNode(animation: animation, colors: colors, scale: scale) self.animatedStickerNode = nil - if let title = title { - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) - } else { - self.titleNode.attributedText = nil - } - - let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) - let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in - return ("URL", contents) - }), textAlignment: .natural) - self.textNode.attributedText = attributedText + if let title = title, text.isEmpty { + self.titleNode.attributedText = nil + let body = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } else { + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } if text.contains("](") { isUserInteractionEnabled = true From 0e1dd66b042aaaa1288372b8f6e0e06bd8b9545e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 17:18:42 +0400 Subject: [PATCH 4/9] Folder improvements --- .../Sources/ChatListController.swift | 3 + .../ChatListFilterPresetController.swift | 535 ++++++++++-------- .../ChatListFilterPresetListController.swift | 2 +- .../FolderInviteLinkListController.swift | 13 +- .../ItemListFolderInviteLinkListItem.swift | 10 +- .../Sources/ChatFolderLinkPreviewScreen.swift | 39 +- 6 files changed, 345 insertions(+), 257 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index defe04e740..79eae1258e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2815,6 +2815,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } + if self.chatListDisplayNode.inlineStackContainerNode != nil { + self.setInlineChatList(location: nil) + } if self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id != folderId { self.chatListDisplayNode.mainContainerNode.switchToFilter(id: .filter(folderId), completion: { completion() diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index f74fa25561..d1a9f01dee 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1053,16 +1053,26 @@ private extension ChatListFilter { } } -func chatListFilterPresetController(context: AccountContext, currentPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { - var currentPreset = currentPreset - +func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { let initialName: String - if let currentPreset = currentPreset { - initialName = currentPreset.title + if let initialPreset { + initialName = initialPreset.title } else { initialName = "" } - let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: currentPreset != nil, includeCategories: currentPreset?.data?.categories ?? [], excludeMuted: currentPreset?.data?.excludeMuted ?? false, excludeRead: currentPreset?.data?.excludeRead ?? false, excludeArchived: currentPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: currentPreset?.data?.excludePeers ?? [], expandedSections: []) + let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: initialPreset != nil, includeCategories: initialPreset?.data?.categories ?? [], excludeMuted: initialPreset?.data?.excludeMuted ?? false, excludeRead: initialPreset?.data?.excludeRead ?? false, excludeArchived: initialPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: initialPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: initialPreset?.data?.excludePeers ?? [], expandedSections: []) + + let updatedCurrentPreset: Signal + if let initialPreset { + updatedCurrentPreset = context.engine.peers.updatedChatListFilters() + |> map { filters -> ChatListFilter? in + return filters.first(where: { $0.id == initialPreset.id }) + } + |> distinctUntilChanged + } else { + updatedCurrentPreset = .single(nil) + } + let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in @@ -1072,7 +1082,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let presentationData = context.sharedContext.currentPresentationData.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: initialPreset?.id ?? -1, title: state.name, emoticon: initialPreset?.emoticon, data: ChatListFilterData(isShared: initialPreset?.data?.isShared ?? false, hasSharedLinks: initialPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) if let data = filter.data { switch chatListFilterType(data) { case .generic: @@ -1114,8 +1124,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var presentInGlobalOverlayImpl: ((ViewController) -> Void)? let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil) - if let currentPreset { - sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id))) + if let initialPreset { + sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id))) } let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:]) @@ -1189,8 +1199,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ), - stateWithPeers |> take(1) - ).start(next: { result, state in + stateWithPeers |> take(1), + updatedCurrentPreset |> take(1) + ).start(next: { result, state, currentPreset in let (accountPeer, limits, premiumLimits) = result let isPremium = accountPeer?.isPremium ?? false @@ -1238,27 +1249,31 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat }) }, openAddExcludePeer: { - let state = stateValue.with { $0 } - var includePeers = ChatListFilterIncludePeers() - includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) - - let _ = (context.engine.peers.currentChatListFilters() - |> deliverOnMainQueue).start(next: { filters in - let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in - skipStateAnimation = true - updateState { state in - var updatedState = state - updatedState.additionallyIncludePeers = filter.data?.includePeers.peers ?? [] - updatedState.additionallyExcludePeers = filter.data?.excludePeers ?? [] - updatedState.includeCategories = filter.data?.categories ?? [] - updatedState.excludeRead = filter.data?.excludeRead ?? false - updatedState.excludeMuted = filter.data?.excludeMuted ?? false - updatedState.excludeArchived = filter.data?.excludeArchived ?? false - return updatedState - } + let _ = (updatedCurrentPreset + |> take(1) + |> deliverOnMainQueue).start(next: { currentPreset in + let state = stateValue.with { $0 } + var includePeers = ChatListFilterIncludePeers() + includePeers.setPeers(state.additionallyIncludePeers) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + + let _ = (context.engine.peers.currentChatListFilters() + |> deliverOnMainQueue).start(next: { filters in + let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in + skipStateAnimation = true + updateState { state in + var updatedState = state + updatedState.additionallyIncludePeers = filter.data?.includePeers.peers ?? [] + updatedState.additionallyExcludePeers = filter.data?.excludePeers ?? [] + updatedState.includeCategories = filter.data?.categories ?? [] + updatedState.excludeRead = filter.data?.excludeRead ?? false + updatedState.excludeMuted = filter.data?.excludeMuted ?? false + updatedState.excludeArchived = filter.data?.excludeArchived ?? false + return updatedState + } + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }, deleteIncludePeer: { peerId in @@ -1323,170 +1338,188 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } }, createLink: { - if currentPreset == nil { + if initialPreset == nil { //TODO:localize let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text = "Please finish creating this folder to share it." presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let state = stateValue.with({ $0 }) + if state.additionallyIncludePeers.isEmpty { + //TODO:localize + let text = "Please add chats to this folder to share it." + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + + return + } + let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) presentControllerImpl?(statusController, nil) applyImpl?(true, { [weak statusController] in let state = stateValue.with({ $0 }) - if let currentPreset, let data = currentPreset.data { - //TODO:localize - var unavailableText: String? - if !data.categories.isEmpty { - unavailableText = "You can’t share folders which have chat types or excluded chats." - } else if data.excludeArchived || data.excludeRead || data.excludeMuted { - unavailableText = "You can’t share folders which have chat types or excluded chats." - } else if !data.excludePeers.isEmpty { - unavailableText = "You can’t share folders which have chat types or excluded chats." - } - if let unavailableText { - statusController?.dismiss() - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - - return - } - - var statusController = statusController - - var previousLink: ExportedChatFolderLink? - openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in - pushControllerImpl?(c) - }, presentController: { c in - presentControllerImpl?(c, nil) - }, completed: { - statusController?.dismiss() - statusController = nil - }, linkUpdated: { updatedLink in - let previousLinkValue = previousLink - previousLink = updatedLink - - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - var links = links ?? [] + let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in + if let currentPreset, let data = currentPreset.data { + //TODO:localize + var unavailableText: String? + if !data.categories.isEmpty { + unavailableText = "You can’t share folders which have chat types or excluded chats." + } else if data.excludeArchived || data.excludeRead || data.excludeMuted { + unavailableText = "You can’t share folders which have chat types or excluded chats." + } else if !data.excludePeers.isEmpty { + unavailableText = "You can’t share folders which have chat types or excluded chats." + } + if let unavailableText { + statusController?.dismiss() - if let updatedLink { - if let index = links.firstIndex(where: { $0.link == updatedLink.link }) { - links[index] = updatedLink - } else { - links.insert(updatedLink, at: 0) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + + return + } + + var statusController = statusController + + var previousLink: ExportedChatFolderLink? + openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in + pushControllerImpl?(c) + }, presentController: { c in + presentControllerImpl?(c, nil) + }, completed: { + statusController?.dismiss() + statusController = nil + }, linkUpdated: { updatedLink in + let previousLinkValue = previousLink + previousLink = updatedLink + + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + var links = links ?? [] + + if let updatedLink { + if let index = links.firstIndex(where: { $0.link == updatedLink.link }) { + links[index] = updatedLink + } else { + links.insert(updatedLink, at: 0) + } + } else if let previousLinkValue { + if let index = links.firstIndex(where: { $0.link == previousLinkValue.link }) { + links.remove(at: index) + } } - } else if let previousLinkValue { - if let index = links.firstIndex(where: { $0.link == previousLinkValue.link }) { - links.remove(at: index) - } - } - sharedLinks.set(.single(links)) + sharedLinks.set(.single(links)) + }) }) - }) - } else { - statusController?.dismiss() - } + } else { + statusController?.dismiss() + } + }) }) } }, openLink: { link in - if let currentPreset, let _ = currentPreset.data { - applyImpl?(false, { - let state = stateValue.with({ $0 }) - pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: link, linkUpdated: { updatedLink in - if updatedLink != link { - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - var links = links ?? [] - - if let updatedLink { - if let index = links.firstIndex(where: { $0.link == link.link }) { - links[index] = updatedLink - } else { - links.insert(updatedLink, at: 0) - } - sharedLinks.set(.single(links)) - } else { - if let index = links.firstIndex(where: { $0.link == link.link }) { - links.remove(at: index) + let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in + if let currentPreset, let _ = currentPreset.data { + applyImpl?(false, { + let state = stateValue.with({ $0 }) + pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: link, linkUpdated: { updatedLink in + if updatedLink != link { + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + var links = links ?? [] + + if let updatedLink { + if let index = links.firstIndex(where: { $0.link == link.link }) { + links[index] = updatedLink + } else { + links.insert(updatedLink, at: 0) + } sharedLinks.set(.single(links)) + } else { + if let index = links.firstIndex(where: { $0.link == link.link }) { + links.remove(at: index) + sharedLinks.set(.single(links)) + } } - } - }) - } - }, presentController: { c in - presentControllerImpl?(c, nil) - })) - }) - } + }) + } + }, presentController: { c in + presentControllerImpl?(c, nil) + })) + }) + } + }) }, removeLink: { link in - if let currentPreset { - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - var links = links ?? [] - - if let index = links.firstIndex(where: { $0.link == link.link }) { - links.remove(at: index) - } - sharedLinks.set(.single(links)) - - actionsDisposable.add(context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: link).start()) - }) - } + let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in + if let currentPreset { + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + var links = links ?? [] + + if let index = links.firstIndex(where: { $0.link == link.link }) { + links.remove(at: index) + } + sharedLinks.set(.single(links)) + + actionsDisposable.add(context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: link).start()) + }) + } + }) }, linkContextAction: { invite, node, gesture in - guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?(), let invite = invite, let currentPreset else { - return - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) + let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in + guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?(), let invite = invite, let currentPreset else { + return + } - //dismissTooltipsImpl?() - - UIPasteboard.general.string = invite.link - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - }))) - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) + var items: [ContextMenuItem] = [] - presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: nil, subject: .chatFolder(slug: invite.slug)), nil) - }))) - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { _, f in - f(.dismissWithoutContent) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + + //dismissTooltipsImpl?() + + UIPasteboard.general.string = invite.link + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }))) - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - var links = links ?? [] - if let index = links.firstIndex(where: { $0.link == invite.link }) { - links.remove(at: index) - } - sharedLinks.set(.single(links)) - }) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: nil, subject: .chatFolder(slug: invite.slug)), nil) + }))) - let _ = (context.engine.peers.editChatFolderLink(filterId: currentPreset.id, link: invite, title: nil, peerIds: nil, revoke: true) - |> deliverOnMainQueue).start(completed: { - let _ = (context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: invite) - |> deliverOnMainQueue).start(completed: { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { _, f in + f(.dismissWithoutContent) + + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + var links = links ?? [] + if let index = links.firstIndex(where: { $0.link == invite.link }) { + links.remove(at: index) + } + sharedLinks.set(.single(links)) }) - }) - }))) - - 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) + + let _ = (context.engine.peers.editChatFolderLink(filterId: currentPreset.id, link: invite, title: nil, peerIds: nil, revoke: true) + |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: invite) + |> deliverOnMainQueue).start(completed: { + }) + }) + }))) + + 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)) @@ -1516,62 +1549,64 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } ) - var attemptNavigationImpl: (() -> Bool)? + var attemptNavigationImpl: ((@escaping (Bool) -> Void) -> Void)? applyImpl = { waitForSync, completed in - let state = stateValue.with { $0 } - - var includePeers = ChatListFilterIncludePeers() - includePeers.setPeers(state.additionallyIncludePeers) - - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filterId = currentPreset?.id ?? -1 - if currentPreset == nil { - filterId = context.engine.peers.generateNewChatListFilterId(filters: filters) - } - var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in + let state = stateValue.with { $0 } - var filters = filters - if let _ = currentPreset { - var found = false - for i in 0 ..< filters.count { - if filters[i].id == updatedFilter.id, case let .filter(_, _, _, data) = filters[i] { - var updatedData = updatedFilter.data ?? data - var includePeers = data.includePeers - includePeers.setPeers(state.additionallyIncludePeers) - updatedData.includePeers = includePeers - updatedFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: updatedData) - filters[i] = updatedFilter - found = true - } + var includePeers = ChatListFilterIncludePeers() + includePeers.setPeers(state.additionallyIncludePeers) + + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filterId = currentPreset?.id ?? -1 + if currentPreset == nil { + filterId = context.engine.peers.generateNewChatListFilterId(filters: filters) } - if !found { - filters = filters.filter { listFilter in - if listFilter.title == updatedFilter.title && listFilter.data == updatedFilter.data { - return false + var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, hasSharedLinks: currentPreset?.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + + var filters = filters + if let _ = currentPreset { + var found = false + for i in 0 ..< filters.count { + if filters[i].id == updatedFilter.id, case let .filter(_, _, _, data) = filters[i] { + var updatedData = updatedFilter.data ?? data + var includePeers = data.includePeers + includePeers.setPeers(state.additionallyIncludePeers) + updatedData.includePeers = includePeers + updatedFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: updatedData) + filters[i] = updatedFilter + found = true } - return true } + if !found { + filters = filters.filter { listFilter in + if listFilter.title == updatedFilter.title && listFilter.data == updatedFilter.data { + return false + } + return true + } + filters.append(updatedFilter) + } + //currentPreset = updatedFilter + } else { filters.append(updatedFilter) } - currentPreset = updatedFilter - } else { - filters.append(updatedFilter) + return filters } - return filters - } - |> deliverOnMainQueue).start(next: { filters in - updated(filters) - - if waitForSync { - let _ = (context.engine.peers.chatListFiltersAreSynced() - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).start(next: { filters in + updated(filters) + + if waitForSync { + let _ = (context.engine.peers.chatListFiltersAreSynced() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + completed() + }) + } else { completed() - }) - } else { - completed() - } + } + }) }) } @@ -1586,16 +1621,23 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ), - sharedLinks.get() + sharedLinks.get(), + updatedCurrentPreset ) |> deliverOnMainQueue - |> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks, currentPreset -> (ItemListControllerState, (ItemListNodeState, Any)) in let (state, includePeers, excludePeers) = stateWithPeers let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - if attemptNavigationImpl?() ?? true { + if let attemptNavigationImpl { + attemptNavigationImpl({ value in + if value { + dismissImpl?() + } + }) + } else { dismissImpl?() } }) @@ -1659,7 +1701,16 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat controller.view.endEditing(true) } controller.attemptNavigation = { _ in - return attemptNavigationImpl?() ?? true + if let attemptNavigationImpl { + attemptNavigationImpl({ value in + if value { + dismissImpl?() + } + }) + return false + } else { + return true + } } let displaySaveAlert: () -> Void = { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -1682,31 +1733,35 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat controller.presentInGlobalOverlay(c) } } - attemptNavigationImpl = { - let state = stateValue.with { $0 } - if let currentPreset = currentPreset, case let .filter(currentId, currentTitle, currentEmoticon, currentData) = currentPreset { - var currentPresetWithoutPinnedPeers = currentPreset - - var currentIncludePeers = ChatListFilterIncludePeers() - currentIncludePeers.setPeers(currentData.includePeers.peers) - var currentPresetWithoutPinnedPeersData = currentData - currentPresetWithoutPinnedPeersData.includePeers = currentIncludePeers - currentPresetWithoutPinnedPeers = .filter(id: currentId, title: currentTitle, emoticon: currentEmoticon, data: currentPresetWithoutPinnedPeersData) - - var includePeers = ChatListFilterIncludePeers() - includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(isShared: currentPreset.data?.isShared ?? false, hasSharedLinks: currentPreset.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) - if currentPresetWithoutPinnedPeers != filter { - displaySaveAlert() - return false + attemptNavigationImpl = { f in + let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in + let state = stateValue.with { $0 } + if let currentPreset = currentPreset, case let .filter(currentId, currentTitle, currentEmoticon, currentData) = currentPreset { + var currentPresetWithoutPinnedPeers = currentPreset + + var currentIncludePeers = ChatListFilterIncludePeers() + currentIncludePeers.setPeers(currentData.includePeers.peers) + var currentPresetWithoutPinnedPeersData = currentData + currentPresetWithoutPinnedPeersData.includePeers = currentIncludePeers + currentPresetWithoutPinnedPeers = .filter(id: currentId, title: currentTitle, emoticon: currentEmoticon, data: currentPresetWithoutPinnedPeersData) + + var includePeers = ChatListFilterIncludePeers() + includePeers.setPeers(state.additionallyIncludePeers) + let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(isShared: currentPreset.data?.isShared ?? false, hasSharedLinks: currentPreset.data?.hasSharedLinks ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + if currentPresetWithoutPinnedPeers != filter { + displaySaveAlert() + f(false) + return + } + } else { + if currentPreset != nil, state.isComplete { + displaySaveAlert() + f(false) + return + } } - } else { - if currentPreset != nil, state.isComplete { - displaySaveAlert() - return false - } - } - return true + f(true) + }) } return controller diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index b8440a7aa9..460e2b3722 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -324,7 +324,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters let id = context.engine.peers.generateNewChatListFilterId(filters: filters) - filters.insert(.filter(id: id, title: title, emoticon: nil, data: data), at: 0) + filters.append(.filter(id: id, title: title, emoticon: nil, data: data)) return filters } |> deliverOnMainQueue).start(next: { _ in diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index f13b364e50..7c889f702d 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -684,7 +684,16 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } else if state.isSaving { doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty, action: { + var saveEnabled = false + if let currentLink = state.currentLink { + if currentLink.title != state.title || Set(currentLink.peerIds) != state.selectedPeerIds { + saveEnabled = true + } + } else { + saveEnabled = true + } + + doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty && saveEnabled, action: { applyChangesImpl?() }) } @@ -739,7 +748,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese f() dismissImpl?() }), - TextAlertAction(type: .defaultAction, title: "Apply", action: { + TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? "Continue" : "Apply", action: { applyChangesImpl?() }) ]), nil) diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift index 5087005653..8173bb57a5 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift @@ -144,6 +144,10 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode private var nonExtractedRect: CGRect? private let offsetContainerNode: ASDisplayNode + public override var controlsContainer: ASDisplayNode { + //return super.controlsContainer + return self.containerNode + } private let iconBackgroundNode: ASDisplayNode private let iconNode: ASImageNode @@ -428,7 +432,9 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + //strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + strongSelf.maskNode.isUserInteractionEnabled = false + strongSelf.addSubnode(strongSelf.maskNode) } let hasCorners = itemListHasRoundedBlockLayout(params) @@ -581,7 +587,7 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - transition.updateSublayerTransformOffset(layer: self.containerNode.layer, offset: CGPoint(x: offset, y: 0.0)) + transition.updateSublayerTransformOffset(layer: self.offsetContainerNode.layer, offset: CGPoint(x: offset + (self.contextSourceNode.isExtractedToContextPreview ? 12.0 : 0.0), y: 0.0)) } override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 12f07d8485..1e7817b01a 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -205,13 +205,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - if topOffset > 0.0 { - topOffset = max(0.0, topOffset) - - if topOffset < topOffsetDistance { - targetContentOffset.pointee.y = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + if scrollView.contentOffset.y <= -100.0 && velocity.y <= -2.0 { + self.environment?.controller()?.dismiss() + } else { + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + if topOffset > 0.0 { + topOffset = max(0.0, topOffset) + + if topOffset < topOffsetDistance { + targetContentOffset.pointee.y = scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + } } } } @@ -287,7 +291,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) } - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + var animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + if self.scrollView.contentOffset.y < 0.0 { + animateOffset += -self.scrollView.contentOffset.y + } self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in @@ -698,10 +705,18 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } var subtitle: String? - if linkContents.alreadyMemberPeerIds.contains(peer.id) { - subtitle = "You are already a member" - } else if let memberCount = linkContents.memberCounts[peer.id] { - subtitle = "\(memberCount) participants" + if case let .channel(channel) = peer, case .broadcast = channel.info { + if linkContents.alreadyMemberPeerIds.contains(peer.id) { + subtitle = "You are already a subscriber" + } else if let memberCount = linkContents.memberCounts[peer.id] { + subtitle = "\(memberCount) subscribers" + } + } else { + if linkContents.alreadyMemberPeerIds.contains(peer.id) { + subtitle = "You are already a member" + } else if let memberCount = linkContents.memberCounts[peer.id] { + subtitle = "\(memberCount) participants" + } } let itemSize = item.update( From 6658280ed8ff805b68ad05adfce764e652da6128 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 17:34:27 +0400 Subject: [PATCH 5/9] Don't count chats that the use can't join --- .../Sources/TelegramEngine/Peers/Communities.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 9298cd5e40..77c7cae42b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -410,7 +410,18 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi var newChatCount = 0 for peerId in peerIds { if transaction.getPeerChatListIndex(peerId) == nil { - newChatCount += 1 + var canJoin = true + if let peer = transaction.getPeer(peerId) { + if let channel = peer as? TelegramChannel { + if case .kicked = channel.participationStatus { + canJoin = false + } + } + } + + if canJoin { + newChatCount += 1 + } } } From 6020b4ed141f8d09d085c98d47ae690a5af7ff4a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 18:09:34 +0400 Subject: [PATCH 6/9] Fix check --- submodules/CheckNode/Sources/CheckNode.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 03259a5ed9..271b207e34 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -391,7 +391,7 @@ public class CheckLayer: CALayer { } } - if !parameters.theme.filledBorder { + if !parameters.theme.filledBorder && !parameters.theme.hasShadow && !parameters.theme.overlayBorder { checkProgress = parameters.animationProgress let fillProgress: CGFloat = parameters.animationProgress @@ -468,8 +468,9 @@ public class CheckLayer: CALayer { context.strokePath() case let .counter(number): - let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor) - text.draw(at: CGPoint()) + let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor.withMultipliedAlpha(parameters.animationProgress)) + let textRect = text.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + text.draw(at: CGPoint(x: UIScreenPixel + textRect.minX + floor((size.width - textRect.width) * 0.5), y: textRect.minY + floor((size.height - textRect.height) * 0.5))) } } } From 4f900cab440665478f24076e57cde6b6e9cbf6a5 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 18:09:50 +0400 Subject: [PATCH 7/9] Update folder link format --- .../Sources/ChatFolderLinkPreviewScreen.swift | 4 ++-- submodules/TelegramUI/Sources/OpenUrl.swift | 4 ++-- submodules/UrlHandling/Sources/UrlHandling.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 1e7817b01a..7747701111 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -474,9 +474,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { chatCountString = "\(linkContents.peers.count) chats" } if let title = linkContents.title { - text = "Do you want to add **\(chatCountString)** to your\nfolder **\(title)**?" + text = "Do you want to add **\(chatCountString)** to the\nfolder **\(title)**?" } else { - text = "Do you want to add **\(chatCountString)** chats to your\nfolder?" + text = "Do you want to add **\(chatCountString)** chats to the\nfolder?" } } } else { diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 95927fdbaa..894951b5c6 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -837,7 +837,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } handleResolvedUrl(.premiumOffer(reference: reference)) - } else if parsedUrl.host == "list" { + } else if parsedUrl.host == "addlist" { if let components = URLComponents(string: "/?" + query) { var slug: String? if let queryItems = components.queryItems { @@ -850,7 +850,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } if let slug = slug { - convertedUrl = "https://t.me/list/\(slug)" + convertedUrl = "https://t.me/addlist/\(slug)" } } } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 758899cecf..b25cd44de7 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -418,7 +418,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { return .theme(pathComponents[1]) - } else if pathComponents[0] == "list" || pathComponents[0] == "folder" { + } else if pathComponents[0] == "addlist" || pathComponents[0] == "folder" { return .chatFolder(slug: pathComponents[1]) } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) { From 361fa693d9a1483edc1808a3407bce0c1f84ef48 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 19:01:03 +0400 Subject: [PATCH 8/9] Folder improvements --- .../Sources/ChatListController.swift | 23 ++++++++++++++++++- .../Sources/ChatFolderLinkPreviewScreen.swift | 4 ++++ .../UrlHandling/Sources/UrlHandling.swift | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 79eae1258e..965ba5181e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1579,11 +1579,32 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return generateTintedImage(image: UIImage(bundleImageName: filterPeersAreMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { + }) + + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.context.engine.peers.updateMultiplePeerMuteSettings(peerIds: data.includePeers.peers, muted: !filterPeersAreMuted) + |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.updateMultiplePeerMuteSettings(peerIds: data.includePeers.peers, muted: !filterPeersAreMuted).start() + let overlayController: UndoOverlayController + if !filterPeersAreMuted { + let iconColor: UIColor = .white + overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [ + "Middle.Group 1.Fill 1": iconColor, + "Top.Group 1.Fill 1": iconColor, + "Bottom.Group 1.Fill 1": iconColor, + "EXAMPLE.Group 1.Fill 1": iconColor, + "Line.Group 1.Stroke 1": iconColor + ], title: nil, text: "All chats in **\(title)** are now muted", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) + } else { + overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_sound_on", scale: 0.056, colors: [:], title: nil, text: "All chats in **\(title)** are now unmuted", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }) + } + strongSelf.present(overlayController, in: .current) }) }))) } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 7747701111..ca8f619933 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -76,6 +76,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } + + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } } final class AnimationHint { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index b25cd44de7..aeefeafe42 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -418,7 +418,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { return .theme(pathComponents[1]) - } else if pathComponents[0] == "addlist" || pathComponents[0] == "folder" { + } else if pathComponents[0] == "addlist" || pathComponents[0] == "folder" || pathComponents[0] == "list" { return .chatFolder(slug: pathComponents[1]) } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) { From 55b5918841d2420bdb5521e3d367daf6ba3b3998 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 4 Apr 2023 23:31:04 +0400 Subject: [PATCH 9/9] Folder improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 ++ .../Sources/AccountContext.swift | 1 + .../ChatListFilterPresetController.swift | 12 ++++ submodules/CheckNode/Sources/CheckNode.swift | 33 +++++---- .../Sources/PremiumLimitScreen.swift | 25 +++++-- .../TelegramEngine/Peers/Communities.swift | 52 ++++++++++++++ .../Sources/ChatFolderLinkPreviewScreen.swift | 70 +++++++++++++++++-- .../Sources/SharedAccountContext.swift | 2 + 8 files changed, 177 insertions(+), 22 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3bff09516c..c250508d09 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9129,3 +9129,7 @@ Sorry for the inconvenience."; "Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)"; "Conversation.OpenChatFolder" = "VIEW CHAT LIST"; + +"Premium.MaxChannelsText" = "You can only join **%1$@** groups and channels. Upgrade to **Telegram Premium** to increase the links limit to **%2$@**."; +"Premium.MaxChannelsNoPremiumText" = "You can only join **%1$@** groups and channels. We are working to let you increase this limit in the future."; +"Premium.MaxChannelsFinalText" = "Sorry, you can only join **%1$@** groups and channels"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index a63259e0ab..3173913004 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -898,6 +898,7 @@ public enum PremiumLimitSubject { case accounts case linksPerSharedFolder case membershipInSharedFolders + case channels } public protocol ComposeController: ViewController { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index d1a9f01dee..57420abf60 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1871,6 +1871,18 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec }) pushController(limitController) + return + case let .tooManyChannels(limit, _): + let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: { + }) + pushController(limitController) + + return + case let .tooManyChannelsInAccount(limit, _): + let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .channels, count: limit, action: { + }) + pushController(limitController) + return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 271b207e34..ca120844c8 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -392,19 +392,26 @@ public class CheckLayer: CALayer { } if !parameters.theme.filledBorder && !parameters.theme.hasShadow && !parameters.theme.overlayBorder { - checkProgress = parameters.animationProgress - - let fillProgress: CGFloat = parameters.animationProgress - - context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0) - - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter))) - context.setBlendMode(.normal) + if parameters.theme.isDottedBorder { + checkProgress = 0.0 + let borderInset = borderWidth / 2.0 + inset + let borderFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: borderInset, dy: borderInset) + context.strokeEllipse(in: borderFrame) + } else { + checkProgress = parameters.animationProgress + + let fillProgress: CGFloat = parameters.animationProgress + + context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0) + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter))) + context.setBlendMode(.normal) + } } else { checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 58ff0a2a17..37b8ed325a 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -800,6 +800,25 @@ private final class LimitSheetContent: CombinedComponent { badgeText = "\(limit)" string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string } + case .channels: + let limit = state.limits.maxChannelsCount + let premiumLimit = state.premiumLimits.maxChannelsCount + iconName = "Premium/Chat" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + if component.count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string + } case .linksPerSharedFolder: /*let count: Int32 = 5 + Int32("".count)// component.count let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks @@ -991,10 +1010,7 @@ private final class LimitSheetContent: CombinedComponent { gloss: isIncreaseButton, animationName: isIncreaseButton ? buttonAnimationName : nil, iconPosition: .right, - action: { [weak component] in - guard let component = component else { - return - } + action: { component.dismiss() if isIncreaseButton { component.action() @@ -1133,6 +1149,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case accounts case linksPerSharedFolder case membershipInSharedFolders + case channels } public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 77c7cae42b..a148a975d6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -32,6 +32,8 @@ public enum ExportChatFolderError { case generic case sharedFolderLimitExceeded(limit: Int32, premiumLimit: Int32) case limitExceeded(limit: Int32, premiumLimit: Int32) + case tooManyChannels(limit: Int32, premiumLimit: Int32) + case tooManyChannelsInAccount(limit: Int32, premiumLimit: Int32) } public struct ExportedChatFolderLink: Equatable { @@ -94,6 +96,21 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String } } } + } else if error.errorDescription == "USER_CHANNELS_TOO_MUCH" || error.errorDescription == "CHANNELS_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(ExportChatFolderError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.tooManyChannelsInAccount(limit: userPremiumLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount)) + } else { + return .fail(.tooManyChannelsInAccount(limit: userDefaultLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount)) + } + } } else { return .fail(.generic) } @@ -391,6 +408,7 @@ public enum JoinChatFolderLinkError { case dialogFilterLimitExceeded(limit: Int32, premiumLimit: Int32) case sharedFolderLimitExceeded(limit: Int32, premiumLimit: Int32) case tooManyChannels(limit: Int32, premiumLimit: Int32) + case tooManyChannelsInAccount(limit: Int32, premiumLimit: Int32) } public final class JoinChatFolderResult { @@ -406,6 +424,25 @@ public final class JoinChatFolderResult { } func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal { + /*#if DEBUG + if "".isEmpty { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.tooManyChannelsInAccount(limit: userPremiumLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount)) + } else { + return .fail(.tooManyChannelsInAccount(limit: userDefaultLimits.maxFolderChatsCount, premiumLimit: userPremiumLimits.maxFolderChatsCount)) + } + } + } + #endif*/ + return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in var newChatCount = 0 for peerId in peerIds { @@ -476,6 +513,21 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi return .fail(.sharedFolderLimitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) } } + } else if error.errorDescription == "CHANNELS_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.tooManyChannelsInAccount(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } else { + return .fail(.tooManyChannelsInAccount(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } + } } else { return .fail(.generic) } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index ca8f619933..2b3f980e32 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -1159,19 +1159,46 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return } + let context = component.context + let navigationController = controller.navigationController as? NavigationController + switch error { case .generic: controller.dismiss() case let .dialogFilterLimitExceeded(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: limit, action: {}) + let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: context, source: .folders)) + }) controller.push(limitController) controller.dismiss() case let .sharedFolderLimitExceeded(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .membershipInSharedFolders, count: limit, action: {}) + let limitController = PremiumLimitScreen(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders)) + }) controller.push(limitController) controller.dismiss() case let .tooManyChannels(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: {}) + let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder)) + }) + controller.push(limitController) + controller.dismiss() + case let .tooManyChannelsInAccount(limit, _): + let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels)) + }) controller.push(limitController) controller.dismiss() } @@ -1391,23 +1418,56 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return } + let context = component.context + let navigationController = controller.navigationController as? NavigationController + //TODO:localize let text: String switch error { case .generic: text = "An error occurred" case let .sharedFolderLimitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders)) }) controller.push(limitController) return case let .limitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: { + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .linksPerSharedFolder)) }) controller.push(limitController) + return + case let .tooManyChannels(limit, _): + let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder)) + }) + controller.push(limitController) + controller.dismiss() + + return + case let .tooManyChannelsInAccount(limit, _): + let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in + guard let navigationController else { + return + } + navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels)) + }) + controller.push(limitController) + controller.dismiss() + return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index aafff67bd3..2d8bcb82c5 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1680,6 +1680,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .linksPerSharedFolder case .membershipInSharedFolders: mappedSubject = .membershipInSharedFolders + case .channels: + mappedSubject = .channels } return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, action: action) }