From e4dc32ad11a6fa319b39b8c795d4ba7dc85e4349 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 3 Apr 2023 19:42:48 +0400 Subject: [PATCH] Folder improvements --- .../Sources/ChatListController.swift | 90 ++- .../ChatListFilterPresetController.swift | 18 +- .../FolderInviteLinkListController.swift | 15 +- .../Sources/InviteLinkHeaderItem.swift | 2 +- .../Sources/IncreaseLimitHeaderItem.swift | 1 + .../Sources/PremiumLimitScreen.swift | 30 +- .../Sources/PremiumLimitsListScreen.swift | 1 + .../QrCodeUI/Sources/QrCodeScreen.swift | 2 +- .../State/UserLimitsConfiguration.swift | 4 +- .../Peers/ChatListFiltering.swift | 8 + .../Peers/TelegramEnginePeers.swift | 13 + .../ChatFolderLinkPreviewScreen/BUILD | 2 + .../Sources/ActionListItemComponent.swift | 196 ++++++ .../Sources/ChatFolderLinkPreviewScreen.swift | 610 ++++++++++++++---- .../Sources/LinkListItemComponent.swift | 330 ++++++++++ .../Sources/PeerListItemComponent.swift | 13 - .../Animations/ChatListCloudFolderLink.tgs | Bin 0 -> 9372 bytes 17 files changed, 1163 insertions(+), 172 deletions(-) create mode 100644 submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift create mode 100644 submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift create mode 100644 submodules/TelegramUI/Resources/Animations/ChatListCloudFolderLink.tgs diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 29f9b3d515..02916252d0 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1392,13 +1392,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } + + let context = strongSelf.context + let filterPeersAreMuted: Signal = strongSelf.context.engine.peers.currentChatListFilters() + |> take(1) + |> mapToSignal { filters -> Signal in + guard let filter = filters.first(where: { $0.id == id }) else { + return .single(false) + } + guard case let .filter(_, _, _, data) = filter else { + return .single(false) + } + return context.engine.data.get( + EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.NotificationSettings.init(id:))) + ) + |> map { list -> Bool in + for item in list { + switch item.muteState { + case .default, .unmuted: + return false + default: + break + } + } + return true + } + } + let _ = combineLatest( queue: Queue.mainQueue(), strongSelf.context.engine.peers.currentChatListFilters(), strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) - ) - ).start(next: { [weak self] filters, premiumLimits in + ), + filterPeersAreMuted + ).start(next: { [weak self] filters, premiumLimits, filterPeersAreMuted in guard let strongSelf = self else { return } @@ -1546,6 +1574,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController for filter in filters { if filter.id == filterId, case let .filter(_, title, _, data) = filter { + if data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty && !data.includePeers.peers.isEmpty { + items.append(.action(ContextMenuActionItem(text: filterPeersAreMuted ? "Unmute All" : "Mute All", textColor: .primary, badge: nil, icon: { theme in + 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).start() + }) + }))) + } + if !data.includePeers.peers.isEmpty && data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty { items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: data.hasSharedLinks ? nil : ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) @@ -2701,13 +2743,53 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) { - openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in + let presentationData = self.presentationData + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.8, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let signal: Signal<[ExportedChatFolderLink]?, NoError> = self.context.engine.peers.getExportedChatFolderLinks(id: filterId) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start(next: { [weak self] links in + guard let self else { + 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) + }) + + /*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/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index e05fb5de5b..8e1856cae5 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1521,22 +1521,10 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat |> deliverOnMainQueue).start(next: { filters in updated(filters) - if let currentPreset, waitForSync { - let _ = (context.engine.peers.updatedChatListFilters() - |> filter { filters -> Bool in - for filter in filters { - if filter.id == currentPreset.id { - if let data = filter.data { - if Set(data.includePeers.peers) == Set(includePeers.peers) { - return true - } - } - } - } - return true - } + if waitForSync { + let _ = (context.engine.peers.chatListFiltersAreSynced() + |> filter { $0 } |> take(1) - |> delay(1.0, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { _ in completed() }) diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 2acde4d64a..f13b364e50 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -166,7 +166,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { let arguments = arguments as! FolderInviteLinkListControllerArguments switch self { case let .header(text): - return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListNewFolder", sectionId: self.section) + return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListCloudFolderLink", sectionId: self.section) case let .mainLinkHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .mainLink(link, isGenerating): @@ -517,13 +517,22 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } else { var isGroup = true + let isPrivate = peer.addressName == nil if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } if isGroup { - text = "You don't have the admin rights to share invite links to this group chat." + if isPrivate { + text = "You don't have the admin rights to share invite links to this private group chat." + } else { + text = "You don't have the admin rights to share invite links to this group chat." + } } else { - text = "You don't have the admin rights to share invite links to this channel." + if isPrivate { + text = "You don't have the admin rights to share invite links to this private channel." + } else { + text = "You don't have the admin rights to share invite links to this channel." + } } } dismissTooltipsImpl?() diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 21969299e6..5ad2da5654 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -151,7 +151,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { if strongSelf.item == nil { - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .count(1), mode: .direct(cachePathPrefix: nil)) strongSelf.animationNode.visibility = true } strongSelf.item = item diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift index e74c76ffec..b0ce7f0abe 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift @@ -197,6 +197,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { badgeIconName: badgeIconName, badgeText: "\(item.count)", badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount), + badgeGraphPosition: CGFloat(item.count) / CGFloat(item.premiumCount), isPremiumDisabled: item.isPremiumDisabled )) let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index bad7273a0f..6eff33fb93 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -44,6 +44,7 @@ private class PremiumLimitAnimationComponent: Component { private let textColor: UIColor private let badgeText: String? private let badgePosition: CGFloat + private let badgeGraphPosition: CGFloat private let isPremiumDisabled: Bool init( @@ -53,6 +54,7 @@ private class PremiumLimitAnimationComponent: Component { textColor: UIColor, badgeText: String?, badgePosition: CGFloat, + badgeGraphPosition: CGFloat, isPremiumDisabled: Bool ) { self.iconName = iconName @@ -61,6 +63,7 @@ private class PremiumLimitAnimationComponent: Component { self.textColor = textColor self.badgeText = badgeText self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition self.isPremiumDisabled = isPremiumDisabled } @@ -83,6 +86,9 @@ private class PremiumLimitAnimationComponent: Component { if lhs.badgePosition != rhs.badgePosition { return false } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } if lhs.isPremiumDisabled != rhs.isPremiumDisabled { return false } @@ -245,7 +251,7 @@ private class PremiumLimitAnimationComponent: Component { let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight)) self.container.frame = containerFrame - let activityPosition: CGFloat = floor(containerFrame.width * component.badgePosition) + let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition) let activeWidth: CGFloat = containerFrame.width - activityPosition if !component.isPremiumDisabled { @@ -433,6 +439,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { let badgeIconName: String? let badgeText: String? let badgePosition: CGFloat + let badgeGraphPosition: CGFloat let isPremiumDisabled: Bool public init( @@ -447,6 +454,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { badgeIconName: String?, badgeText: String?, badgePosition: CGFloat, + badgeGraphPosition: CGFloat, isPremiumDisabled: Bool ) { self.inactiveColor = inactiveColor @@ -460,6 +468,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { self.badgeIconName = badgeIconName self.badgeText = badgeText self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition self.isPremiumDisabled = isPremiumDisabled } @@ -497,6 +506,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { if lhs.badgePosition != rhs.badgePosition { return false } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } if lhs.isPremiumDisabled != rhs.isPremiumDisabled { return false } @@ -524,6 +536,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { textColor: component.activeTitleColor, badgeText: component.badgeText, badgePosition: component.badgePosition, + badgeGraphPosition: component.badgeGraphPosition, isPremiumDisabled: component.isPremiumDisabled ), availableSize: CGSize(width: context.availableSize.width, height: height), @@ -591,7 +604,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { transition: context.transition ) - let activityPosition = floor(context.availableSize.width * component.badgePosition) + let activityPosition = floor(context.availableSize.width * component.badgeGraphPosition) var inactiveValueOpacity: CGFloat = 1.0 if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 { @@ -747,6 +760,7 @@ private final class LimitSheetContent: CombinedComponent { let defaultValue: String let premiumValue: String let badgePosition: CGFloat + let badgeGraphPosition: CGFloat switch subject { case .folders: let limit = state.limits.maxFoldersCount @@ -757,6 +771,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if !state.isPremium && badgePosition > 0.5 { string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string @@ -775,6 +790,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if isPremiumDisabled { badgeText = "\(limit)" @@ -794,6 +810,11 @@ private final class LimitSheetContent: CombinedComponent { string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string defaultValue = count > limit ? "\(limit)" : "" premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)" + if count >= premiumLimit { + badgeGraphPosition = max(0.1, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) + } badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) if isPremiumDisabled { @@ -809,6 +830,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if isPremiumDisabled { badgeText = "\(limit)" @@ -823,6 +845,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if isPremiumDisabled { badgeText = "\(limit)" @@ -837,6 +860,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" badgePosition = component.count == 4 ? 1.0 : 0.5 + badgeGraphPosition = badgePosition titleText = strings.Premium_FileTooLarge if isPremiumDisabled { @@ -856,6 +880,7 @@ private final class LimitSheetContent: CombinedComponent { } else { badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit)) } + badgeGraphPosition = badgePosition buttonAnimationName = "premium_addone" if isPremiumDisabled { @@ -931,6 +956,7 @@ private final class LimitSheetContent: CombinedComponent { badgeIconName: iconName, badgeText: badgeText, badgePosition: badgePosition, + badgeGraphPosition: badgeGraphPosition, isPremiumDisabled: isPremiumDisabled ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index d9340ea5e4..eb13f43ba2 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -170,6 +170,7 @@ private final class LimitComponent: CombinedComponent { badgeIconName: "", badgeText: nil, badgePosition: 0.0, + badgeGraphPosition: 0.0, isPremiumDisabled: false ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 1cadfcf3c5..86330a046b 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -241,7 +241,7 @@ public final class QrCodeScreen: ViewController { case .chatFolder: //TODO:localize title = "Invite by QR Code" - text = "Everyone on Telegram can scan this code to join your folder." + text = "Everyone on Telegram can scan this code to add this folder and join the chats included in this invite link." default: title = "" text = "" diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index af33514b6a..70eb29f827 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -107,7 +107,7 @@ extension UserLimitsConfiguration { self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength) self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText) self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1) - self.maxSharedFolderInviteLinks = getValue("chatlists_invites_limit", orElse: isPremium ? 100 : 3) - self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2) + self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3) + self.maxSharedFolderJoin = getValue("chatlist_joined_limit", orElse: isPremium ? 100 : 2) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index c6acb96500..5995ab2615 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -1319,6 +1319,14 @@ func requestChatListFiltersSync(transaction: Transaction) { transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeChatListFiltersOperation(content: .sync)) } +func _internal_chatListFiltersAreSynced(postbox: Postbox) -> Signal { + return postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizeChatListFilters, limit: 1) + |> map { view -> Bool in + return view.entries.isEmpty + } + |> distinctUntilChanged +} + func managedChatListFilters(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { return Signal { _ in let updateFeaturedDisposable = _internal_updateChatListFeaturedFilters(postbox: postbox, network: network).start() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 8550402677..2ece79bff4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -245,6 +245,15 @@ public extension TelegramEngine { public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal { return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval) } + + public func updateMultiplePeerMuteSettings(peerIds: [EnginePeer.Id], muted: Bool) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for peerId in peerIds { + _internal_updatePeerMuteSetting(account: self.account, transaction: transaction, peerId: peerId, threadId: nil, muteInterval: muted ? Int32.max : nil) + } + } + |> ignoreValues + } public func updatePeerDisplayPreviewsSetting(peerId: PeerId, threadId: Int64?, displayPreviews: PeerNotificationDisplayPreviews) -> Signal { return _internal_updatePeerDisplayPreviewsSetting(account: self.account, peerId: peerId, threadId: threadId, displayPreviews: displayPreviews) @@ -512,6 +521,10 @@ public extension TelegramEngine { public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> { return _internal_updatedChatListFilters(postbox: self.account.postbox, hiddenIds: self.account.viewTracker.hiddenChatListFilterIds) } + + public func chatListFiltersAreSynced() -> Signal { + return _internal_chatListFiltersAreSynced(postbox: self.account.postbox) + } public func updatedChatListFiltersInfo() -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { return _internal_updatedChatListFiltersInfo(postbox: self.account.postbox) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 1572124149..585ce7411f 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/Markdown", "//submodules/UndoUI", "//submodules/PremiumUI", + "//submodules/QrCodeUI", + "//submodules/InviteLinksUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift new file mode 100644 index 0000000000..9e4fa31343 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift @@ -0,0 +1,196 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData + +final class ActionListItemComponent: Component { + let theme: PresentationTheme + let sideInset: CGFloat + let iconName: String? + let title: String + let hasNext: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + sideInset: CGFloat, + iconName: String?, + title: String, + hasNext: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.sideInset = sideInset + self.iconName = iconName + self.title = title + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let iconView: UIImageView + private let separatorLayer: SimpleLayer + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: ActionListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + self.iconView = UIImageView() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + + self.containerButton.addSubview(self.iconView) + + self.containerButton.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + if self.component?.iconName != component.iconName { + if let iconName = component.iconName { + self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate) + } else { + self.iconView.image = nil + } + } + if themeUpdated { + self.iconView.tintColor = component.theme.list.itemAccentColor + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 44.0 + let verticalInset: CGFloat = 1.0 + let leftInset: CGFloat = 62.0 + component.sideInset + let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + + let previousTitleFrame = self.title.view?.frame + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let centralContentHeight: CGFloat = titleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + } + + if let iconImage = self.iconView.image { + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + 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/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 136936f094..10536d30d5 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -17,6 +17,9 @@ import Markdown import UndoUI import PremiumUI import ButtonComponent +import ContextUI +import QrCodeUI +import InviteLinksUI private final class ChatFolderLinkPreviewScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -102,6 +105,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { private var selectedItems = Set() + private var linkListItems: [ExportedChatFolderLink] = [] + private let bottomOverscrollLimit: CGFloat private var ignoreScrolling: Bool = false @@ -324,6 +329,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } } + if self.component == nil, case let .linkList(_, initialLinks) = component.subject { + self.linkListItems = initialLinks + } + self.component = component self.state = state self.environment = environment @@ -364,7 +373,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let titleString: String var allChatsAdded = false - if let linkContents = component.linkContents { + if case .linkList = component.subject { + //TODO:localize + titleString = "Share Folder" + } else if let linkContents = component.linkContents { //TODO:localize if case .remove = component.subject { titleString = "Remove Folder" @@ -406,7 +418,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentHeight += 14.0 var topBadge: String? - if case .remove = component.subject { + if case .linkList = component.subject { + } else if case .remove = component.subject { } else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil { topBadge = "+\(linkContents.peers.count)" } @@ -435,7 +448,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentHeight += 20.0 let text: String - if let linkContents = component.linkContents { + if case .linkList = component.subject { + text = "Create more links to set up different access\nlevels for different people." + } else if let linkContents = component.linkContents { if case .remove = component.subject { text = "Do you also want to quit the chats included in this folder?" } else if allChatsAdded { @@ -493,90 +508,257 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { var itemsHeight: CGFloat = 0.0 var validIds: [AnyHashable] = [] - if let linkContents = component.linkContents { + if case let .linkList(folderId, _) = component.subject { + do { + let id = AnyHashable("action") + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item + } + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(ActionListItemComponent( + theme: environment.theme, + sideInset: 0.0, + iconName: "Contact List/LinkActionIcon", + title: "Create a New Link", + hasNext: !self.linkListItems.isEmpty, + action: { [weak self] in + self?.openCreateLink() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height + } + + for i in 0 ..< self.linkListItems.count { + let link = self.linkListItems[i] + + let id = AnyHashable(link.link) + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item + } + + let subtitle: String + if link.peerIds.count == 1 { + subtitle = "includes 1 chat" + } else { + subtitle = "includes \(link.peerIds.count) chats" + } + + let itemComponent = LinkListItemComponent( + theme: environment.theme, + sideInset: 0.0, + title: link.title.isEmpty ? link.link : link.title, + link: link, + label: subtitle, + selectionState: .none, + hasNext: i != self.linkListItems.count - 1, + action: { [weak self] link in + guard let self else { + return + } + self.openLink(link: link) + }, + contextAction: { [weak self] link, sourceView, gesture in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + + itemList.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: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = link.link + + if let self, let component = self.component, let controller = self.environment?.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + }))) + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component, let controller = self.environment?.controller() { + controller.present(QrCodeScreen(context: component.context, updatedPresentationData: nil, subject: .chatFolder(slug: link.slug)), in: .window(.root)) + } + }))) + + itemList.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: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component { + self.linkListItems.removeAll(where: { $0.link == link.link }) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + + let context = component.context + let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true) + |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.deleteChatFolderLink(filterId: folderId, link: link) + |> deliverOnMainQueue).start(completed: { + }) + }) + } + }))) + + let items = ContextController.Items(content: .list(itemList)) + + let controller = ContextController( + account: component.context.account, + presentationData: presentationData, + source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)), + items: .single(items), + recognizer: nil, + gesture: gesture + ) + + environment.controller()?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + environment.controller()?.presentInGlobalOverlay(controller) + } + ) + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(itemComponent), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height + } + } else if let linkContents = component.linkContents { for i in 0 ..< linkContents.peers.count { let peer = linkContents.peers[i] - for _ in 0 ..< 1 { - //let id: AnyHashable = AnyHashable("\(peer.id)_\(j)") - let id = AnyHashable(peer.id) - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - self.items[id] = item - } - - 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" - } - - let itemSize = item.update( - transition: itemTransition, - component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - sideInset: 0.0, - title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer, - subtitle: subtitle, - selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)), - hasNext: i != linkContents.peers.count - 1, - action: { [weak self] peer in - guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { - return - } - - if case .remove = component.subject { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = "You are already a member of this channel." - } else { - text = "You are already a member of this group." - } - controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) - } else { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height + let id = AnyHashable(peer.id) + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item } + + 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" + } + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: 0.0, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + subtitle: subtitle, + selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)), + hasNext: i != linkContents.peers.count - 1, + action: { [weak self] peer in + guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { + return + } + + if case .remove = component.subject { + if self.selectedItems.contains(peer.id) { + self.selectedItems.remove(peer.id) + } else { + self.selectedItems.insert(peer.id) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = "You are already a member of this channel." + } else { + text = "You are already a member of this group." + } + controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) + } else { + if self.selectedItems.contains(peer.id) { + self.selectedItems.remove(peer.id) + } else { + self.selectedItems.insert(peer.id) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height } } @@ -592,7 +774,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } let listHeaderTitle: String - if let linkContents = component.linkContents { + if case .linkList = component.subject { + listHeaderTitle = "INVITE LINKS" + } else if let linkContents = component.linkContents { if case .remove = component.subject { if linkContents.peers.count == 1 { listHeaderTitle = "1 CHAT TO QUIT" @@ -839,21 +1023,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } controller.dismiss() - - /*self.joinDisposable = (component.context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(self.selectedItems)) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - })*/ } else if allChatsAdded { controller.dismiss() } else if let _ = component.linkContents { if self.joinDisposable == nil, !self.selectedItems.isEmpty { let joinSignal: Signal switch component.subject { - case .remove: + case .linkList, .remove: return case let .slug(slug): joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems)) @@ -961,40 +1137,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) - /*let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(SolidRoundedButtonComponent( - title: actionButtonTitle, - badge: (self.selectedItems.isEmpty || allChatsAdded) ? nil : "\(self.selectedItems.count)", - theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 11.0, - gloss: false, - isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, - animationName: nil, - iconPosition: .right, - iconSpacing: 4.0, - isLoading: self.inProgress, - action: { [weak self] in - - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) - )*/ - let bottomPanelHeight = 14.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) - } - transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight))) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + var bottomPanelHeight: CGFloat = 0.0 + + if case .linkList = component.subject { + bottomPanelHeight += 30.0 + } else { + bottomPanelHeight += 14.0 + environment.safeInsets.bottom + actionButtonSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight))) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + } if let controller = environment.controller() { let subLayout = ContainerViewLayout( @@ -1015,9 +1175,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let containerInset: CGFloat = environment.statusBarHeight + 10.0 let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) - let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset) - - //self.scrollContentClippingView.layer.cornerRadius = 10.0 + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight) @@ -1026,12 +1184,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 8.0 - (containerInset + 56.0))) + let scrollClippingFrame: CGRect + if case .linkList = component.subject { + scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - (containerInset + 56.0) + 1000.0)) + } else { + scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - bottomPanelHeight - 8.0 - (containerInset + 56.0))) + } transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset))) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize @@ -1044,6 +1207,168 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return availableSize } + + private func openLink(link: ExportedChatFolderLink) { + guard let component = self.component else { + return + } + guard case let .linkList(folderId, _) = component.subject else { + return + } + + let _ = (component.context.engine.peers.currentChatListFilters() + |> deliverOnMainQueue).start(next: { [weak self] filters in + guard let self, let component = self.component else { + return + } + guard let filter = filters.first(where: { $0.id == folderId }) else { + return + } + guard case let .filter(_, title, _, data) = filter else { + return + } + + let peerIds = data.includePeers.peers + let _ = (component.context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + let peers = peers.compactMap({ peer -> EnginePeer? in + guard let peer else { + return nil + } + if case let .legacyGroup(group) = peer, group.migrationReference != nil { + return nil + } + return peer + }) + + let navigationController = controller.navigationController + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in + (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + })) + controller.dismiss() + }) + }) + } + + private func openCreateLink() { + guard let component = self.component else { + return + } + guard case let .linkList(folderId, _) = component.subject else { + return + } + + let _ = (component.context.engine.peers.currentChatListFilters() + |> deliverOnMainQueue).start(next: { [weak self] filters in + guard let self, let component = self.component else { + return + } + guard let filter = filters.first(where: { $0.id == folderId }) else { + return + } + guard case let .filter(_, title, _, data) = filter else { + return + } + + let peerIds = data.includePeers.peers + let _ = (component.context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + let peers = peers.compactMap({ peer -> EnginePeer? in + guard let peer else { + return nil + } + if case let .legacyGroup(group) = peer, group.migrationReference != nil { + return nil + } + return peer + }) + if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { + let navigationController = controller.navigationController + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: { _ in }, presentController: { [weak navigationController] c in + (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + })) + } else { + var enabledPeerIds: [EnginePeer.Id] = [] + for peer in peers { + if canShareLinkToPeer(peer: peer) { + enabledPeerIds.append(peer.id) + } + } + + let _ = (component.context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: enabledPeerIds) + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + self.linkListItems.insert(link, at: 0) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + + let navigationController = controller.navigationController + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { [weak self] updatedLink in + guard let self else { + return + } + if let index = self.linkListItems.firstIndex(where: { $0.link == link.link }) { + if let updatedLink { + self.linkListItems[index] = updatedLink + } else { + self.linkListItems.remove(at: index) + } + } else { + if let updatedLink { + self.linkListItems.insert(updatedLink, at: 0) + } + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + }, presentController: { [weak navigationController] c in + (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + })) + + controller.dismiss() + }, error: { [weak self] error in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + //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: { + }) + + controller.push(limitController) + + return + case let .limitExceeded(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: { + }) + controller.push(limitController) + + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + } + }) + }) + } } func makeView() -> View { @@ -1060,6 +1385,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { case slug(String) case updates(ChatFolderUpdates) case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id]) + case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink]) } private let context: AccountContext @@ -1116,3 +1442,25 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { } } } + +private final class LinkListContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift new file mode 100644 index 0000000000..ae5ece266c --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift @@ -0,0 +1,330 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import TelegramCore +import CheckNode + +func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } +} + +final class LinkListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + let theme: PresentationTheme + let sideInset: CGFloat + let title: String + let link: ExportedChatFolderLink + let label: String + let selectionState: SelectionState + let hasNext: Bool + let action: (ExportedChatFolderLink) -> Void + let contextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void + + init( + theme: PresentationTheme, + sideInset: CGFloat, + title: String, + link: ExportedChatFolderLink, + label: String, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (ExportedChatFolderLink) -> Void, + contextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void + ) { + self.theme = theme + self.sideInset = sideInset + self.title = title + self.link = link + self.label = label + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + self.contextAction = contextAction + } + + static func ==(lhs: LinkListItemComponent, rhs: LinkListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.link != rhs.link { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: ContextControllerSourceView { + private let extractedContainerView: ContextExtractedContentContainingView + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let iconView: UIImageView + private let iconBackgroundView: UIImageView + + private var checkLayer: CheckLayer? + + private var isExtractedToContextMenu: Bool = false + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: LinkListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.extractedContainerView = ContextExtractedContentContainingView() + self.containerButton = HighlightTrackingButton() + + self.iconView = UIImageView() + self.iconBackgroundView = UIImageView() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.addSubview(self.containerButton) + + self.containerButton.addSubview(self.iconBackgroundView) + self.containerButton.addSubview(self.iconView) + + self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + self.containerButton.clipsToBounds = value + self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil + self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 + } + self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in + guard let self else { + return + } + self.isExtractedToContextMenu = value + + let mappedTransition: Transition + if value { + mappedTransition = Transition(transition) + } else { + mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + self.state?.updated(transition: mappedTransition) + } + + self.containerButton.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted, case .none = component.selectionState { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction(component.link, self.extractedContainerView, gesture) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(component.link) + } + + func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0 + + let height: CGFloat = 60.0 + let verticalInset: CGFloat = 1.0 + var leftInset: CGFloat = 62.0 + component.sideInset + var iconLeftInset: CGFloat = component.sideInset + + if case let .editing(isSelected) = component.selectionState { + leftInset += 48.0 + iconLeftInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset + + if themeUpdated { + self.iconBackgroundView.image = generateFilledCircleImage(diameter: 40.0, color: component.theme.list.itemCheckColors.fillColor) + self.iconView.image = UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = component.theme.list.itemCheckColors.foregroundColor + } + + if let iconImage = self.iconView.image { + transition.setFrame(view: self.iconBackgroundView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - 40.0) * 0.5), y: floor((height - 40.0) * 0.5)), size: CGSize(width: 40.0, height: 40.0))) + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconImage.size.width) * 0.5), y: floor((height - iconImage.size.height) * 0.5)), size: iconImage.size)) + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.label, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSpacing: CGFloat = 1.0 + + let contentHeight: CGFloat = titleSize.height + titleSpacing + labelSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - contentHeight) / 2.0)), size: titleSize) + let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + titleView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + labelView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(labelView) + } + transition.setPosition(view: labelView, position: labelFrame.origin) + labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.extractedContainerView, frame: resultBounds) + transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds) + self.extractedContainerView.contentRect = resultBounds + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + 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/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift index 1ee2e163ef..4f8e5a3ad9 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift @@ -15,19 +15,6 @@ import TelegramStringFormatting private let avatarFont = avatarPlaceholderFont(size: 15.0) -private func cancelContextGestures(view: UIView) { - if let gestureRecognizers = view.gestureRecognizers { - for gesture in gestureRecognizers { - if let gesture = gesture as? ContextGesture { - gesture.cancel() - } - } - } - for subview in view.subviews { - cancelContextGestures(view: subview) - } -} - final class PeerListItemComponent: Component { enum SelectionState: Equatable { case none diff --git a/submodules/TelegramUI/Resources/Animations/ChatListCloudFolderLink.tgs b/submodules/TelegramUI/Resources/Animations/ChatListCloudFolderLink.tgs new file mode 100644 index 0000000000000000000000000000000000000000..3e71d25b4086c5c420c09f6373579e7775cdeeda GIT binary patch literal 9372 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 W&rP%L&{E;~um2Aa!?@kjyZ``a=M2^W literal 0 HcmV?d00001