diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5eacbd56e9..bf6bd7259f 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1544,7 +1544,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController //TODO:localize for filter in filters { - if filter.id == filterId, case let .filter(_, _, _, data) = filter { + if filter.id == filterId, case let .filter(_, title, _, data) = filter { if !data.includePeers.peers.isEmpty { items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) @@ -1553,7 +1553,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.shareFolder(filterId: filterId, data: data) + strongSelf.shareFolder(filterId: filterId, data: data, title: title) }) }))) } @@ -2699,9 +2699,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - private func shareFolder(filterId: Int32, data: ChatListFilterData) { - self.push(folderInviteLinkListController(context: self.context, filterId: filterId, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { _ in - })) + private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) { + /*self.push(folderInviteLinkListController(context: self.context, filterId: filterId, title: title, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { _ in + }))*/ } private func askForFilterRemoval(id: Int32) { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index d78e95e6a1..709140317e 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -317,7 +317,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case includeExpand(String) case excludeExpand(String) case inviteLinkHeader - case inviteLinkCreate + case inviteLinkCreate(hasLinks: Bool) case inviteLink(Int, ExportedChatFolderLink) case inviteLinkInfo @@ -518,10 +518,10 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { }) case .inviteLinkHeader: //TODO:localize - return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", sectionId: self.section) - case .inviteLinkCreate: + return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section) + case let.inviteLinkCreate(hasLinks): //TODO:localize - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "Share Folder with Others", sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? "Create a New Link" : "Share Folder", sectionId: self.section, editing: false, action: { arguments.createLink() }) case let .inviteLink(_, link): @@ -532,7 +532,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { } case .inviteLinkInfo: //TODO:localize - return ItemListTextItem(presentationData: presentationData, text: .markdown("Give vour friends and colleagues access to the entire folder including all of its groups and channels where you have the necessary rights."), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .markdown("Share access to some of this folder's groups and channels with others."), sectionId: self.section) } } } @@ -575,7 +575,7 @@ private struct ChatListFilterPresetControllerState: Equatable { } } -private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?) -> [ChatListFilterPresetEntry] { +private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, currentPreset: ChatListFilter?, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?) -> [ChatListFilterPresetEntry] { var entries: [ChatListFilterPresetEntry] = [] if isNewFilter { @@ -614,46 +614,49 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio entries.append(.includePeerInfo(presentationData.strings.ChatListFolder_IncludeSectionInfo)) - entries.append(.excludePeersHeader(presentationData.strings.ChatListFolder_ExcludedSectionHeader)) - entries.append(.addExcludePeer(title: presentationData.strings.ChatListFolder_AddChats)) - - var excludeCategoryIndex = 0 - for category in ChatListFilterExcludeCategory.allCases { - let isExcluded: Bool - switch category { - case .read: - isExcluded = state.excludeRead - case .muted: - isExcluded = state.excludeMuted - case .archived: - isExcluded = state.excludeArchived + if let currentPreset, let data = currentPreset.data, data.isShared { + } else { + entries.append(.excludePeersHeader(presentationData.strings.ChatListFolder_ExcludedSectionHeader)) + entries.append(.addExcludePeer(title: presentationData.strings.ChatListFolder_AddChats)) + + var excludeCategoryIndex = 0 + for category in ChatListFilterExcludeCategory.allCases { + let isExcluded: Bool + switch category { + case .read: + isExcluded = state.excludeRead + case .muted: + isExcluded = state.excludeMuted + case .archived: + isExcluded = state.excludeArchived + } + + if isExcluded { + entries.append(.excludeCategory(index: excludeCategoryIndex, category: category, title: category.title(strings: presentationData.strings), isRevealed: state.revealedItemId == .excludeCategory(category))) + } + excludeCategoryIndex += 1 } - if isExcluded { - entries.append(.excludeCategory(index: excludeCategoryIndex, category: category, title: category.title(strings: presentationData.strings), isRevealed: state.revealedItemId == .excludeCategory(category))) - } - excludeCategoryIndex += 1 - } - - if !excludePeers.isEmpty { - var count = 0 - for peer in excludePeers { - entries.append(.excludePeer(index: entries.count, peer: peer, isRevealed: state.revealedItemId == .peer(peer.peerId))) - count += 1 - if excludePeers.count >= 7 && count == 5 && !state.expandedSections.contains(.exclude) { - break + if !excludePeers.isEmpty { + var count = 0 + for peer in excludePeers { + entries.append(.excludePeer(index: entries.count, peer: peer, isRevealed: state.revealedItemId == .peer(peer.peerId))) + count += 1 + if excludePeers.count >= 7 && count == 5 && !state.expandedSections.contains(.exclude) { + break + } + } + if count < excludePeers.count { + entries.append(.excludeExpand(presentationData.strings.ChatListFilter_ShowMoreChats(Int32(excludePeers.count - count)))) } } - if count < excludePeers.count { - entries.append(.excludeExpand(presentationData.strings.ChatListFilter_ShowMoreChats(Int32(excludePeers.count - count)))) - } + + entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) } - entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) - if !isNewFilter, let inviteLinks { entries.append(.inviteLinkHeader) - entries.append(.inviteLinkCreate) + entries.append(.inviteLinkCreate(hasLinks: !inviteLinks.isEmpty)) var index = 0 for link in inviteLinks { @@ -691,38 +694,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let additionalCategories: [ChatListNodeAdditionalCategory] = [ - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.contacts.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), - title: presentationData.strings.ChatListFolder_CategoryContacts - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.nonContacts.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow), - title: presentationData.strings.ChatListFolder_CategoryNonContacts - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.groups.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), cornerRadius: 12.0, color: .green), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .green), - title: presentationData.strings.ChatListFolder_CategoryGroups - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.channels.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: 12.0, color: .red), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .red), - title: presentationData.strings.ChatListFolder_CategoryChannels - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.bots.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), cornerRadius: 12.0, color: .violet), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .violet), - title: presentationData.strings.ChatListFolder_CategoryBots - ) - ] + var additionalCategories: [ChatListNodeAdditionalCategory] = [] var selectedCategories = Set() let categoryMapping: [ChatListFilterPeerCategories: AdditionalCategoryId] = [ .contacts: .contacts, @@ -731,9 +703,46 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f .channels: .channels, .bots: .bots ] - for (category, id) in categoryMapping { - if filterData.categories.contains(category) { - selectedCategories.insert(id.rawValue) + + if let data = filter.data, data.isShared { + } else { + additionalCategories = [ + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.contacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), + title: presentationData.strings.ChatListFolder_CategoryContacts + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.nonContacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow), + title: presentationData.strings.ChatListFolder_CategoryNonContacts + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.groups.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), cornerRadius: 12.0, color: .green), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .green), + title: presentationData.strings.ChatListFolder_CategoryGroups + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.channels.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: 12.0, color: .red), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .red), + title: presentationData.strings.ChatListFolder_CategoryChannels + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.bots.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), cornerRadius: 12.0, color: .violet), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .violet), + title: presentationData.strings.ChatListFolder_CategoryBots + ) + ] + + for (category, id) in categoryMapping { + if filterData.categories.contains(category) { + selectedCategories.insert(id.rawValue) + } } } @@ -1059,8 +1068,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil) if let currentPreset { - sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id) - |> map(Optional.init))) + sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id))) } let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:]) @@ -1265,23 +1273,80 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } }, createLink: { - if let currentPreset, let data = currentPreset.data, !data.includePeers.peers.isEmpty { - pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { updatedLink in - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - guard var links else { - return - } - - if let updatedLink { - links.insert(updatedLink, at: 0) - sharedLinks.set(.single(links)) - } - }) - })) + let state = stateValue.with({ $0 }) + + if let currentPreset, !state.additionallyIncludePeers.isEmpty { + let _ = (context.engine.data.get( + EngineDataList(state.additionallyIncludePeers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { peers in + let peers = peers.compactMap({ $0 }) + if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { + pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: nil, linkUpdated: { updatedLink in + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + guard var links else { + return + } + + if let updatedLink { + links.insert(updatedLink, at: 0) + sharedLinks.set(.single(links)) + } + }) + })) + } else { + let _ = (context.engine.peers.exportChatFolder(filterId: currentPreset.id, title: "", peerIds: state.additionallyIncludePeers) + |> deliverOnMainQueue).start(next: { link in + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + guard var links else { + return + } + + links.insert(link, at: 0) + sharedLinks.set(.single(links)) + }) + + 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 + guard var links else { + return + } + + if let updatedLink { + if let index = links.firstIndex(where: { $0 == link }) { + links.remove(at: index) + } + links.insert(updatedLink, at: 0) + sharedLinks.set(.single(links)) + } else { + if let index = links.firstIndex(where: { $0 == link }) { + links.remove(at: index) + sharedLinks.set(.single(links)) + } + } + }) + } + })) + }, error: { error in + //TODO:localize + let text: String + switch error { + case .generic: + text = "An error occurred" + case .limitExceeded: + text = "You can't create more links." + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }) + } + }) } }, openLink: { link in - if let currentPreset, let data = currentPreset.data { - pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: link, linkUpdated: { updatedLink in + if let currentPreset, let _ = currentPreset.data { + 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 guard var links else { @@ -1387,7 +1452,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(currentPreset != nil ? presentationData.strings.ChatListFolder_TitleEdit : presentationData.strings.ChatListFolder_TitleCreate), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, state: state, includePeers: includePeers, excludePeers: excludePeers, isPremium: isPremium, limit: premiumLimits.maxFolderChatsCount, inviteLinks: sharedLinks), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, currentPreset: currentPreset, state: state, includePeers: includePeers, excludePeers: excludePeers, isPremium: isPremium, limit: premiumLimits.maxFolderChatsCount, inviteLinks: sharedLinks), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation) skipStateAnimation = false return (controllerState, (listState, arguments)) diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index 05fbee64cb..bc125a6266 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/LocalizedPeerData:LocalizedPeerData", "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", "//submodules/QrCodeUI:QrCodeUI", + "//submodules/PromptUI", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index cfbea0bd96..d0c5ececc4 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -21,6 +21,7 @@ import ItemListPeerItem import ShareController import UndoUI import QrCodeUI +import PromptUI private final class FolderInviteLinkListControllerArguments { let context: AccountContext @@ -169,16 +170,20 @@ private enum InviteLinksListEntry: ItemListNodeEntry { case let .mainLinkHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .mainLink(link, isGenerating): - return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: link != nil ? "Share Invite Link" : "Generate Invite Link", displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { + return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: link != nil ? "Copy" : "Generate Invite Link", secondaryButtonTitle: link != nil ? "Share" : nil, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { if let link { arguments.copyLink(link.link) } }, shareAction: { if let link { - arguments.shareMainLink(link.link) + arguments.copyLink(link.link) } else { arguments.generateLink() } + }, secondaryAction: { + if let link { + arguments.shareMainLink(link.link) + } }, contextAction: { node, gesture in arguments.mainLinkContextAction(link, node, gesture) }, viewAction: { @@ -205,6 +210,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled), enabled: true, selectable: true, + highlightable: false, sectionId: self.section, action: { arguments.peerAction(peer, isEnabled) @@ -218,46 +224,44 @@ private enum InviteLinksListEntry: ItemListNodeEntry { } } -private func canShareLinkToPeer(peer: EnginePeer) -> Bool { - var isEnabled = false - switch peer { - case let .channel(channel): - if channel.hasPermission(.inviteMembers) { - isEnabled = true - } else if channel.username != nil { - isEnabled = true - } - default: - break - } - return isEnabled -} - private func folderInviteLinkListControllerEntries( presentationData: PresentationData, state: FolderInviteLinkListControllerState, + title: String, allPeers: [EnginePeer] ) -> [InviteLinksListEntry] { var entries: [InviteLinksListEntry] = [] + //TODO:localize + + var infoString: String? let chatCountString: String let peersHeaderString: String - if state.selectedPeerIds.isEmpty { - chatCountString = "Anyone with this link can add Gaming Club folder and the chats selected below." + + let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) + + if !canShareChats { + infoString = "You can only share groups and channels in which you are allowed to create invite links." + chatCountString = "There are no chats in this folder that you can share with others." + peersHeaderString = "THESE CHATS CANNOT BE SHARED" + } else if state.selectedPeerIds.isEmpty { + chatCountString = "Anyone with this link can add \(title) folder and the chats selected below." peersHeaderString = "CHATS" } else if state.selectedPeerIds.count == 1 { - chatCountString = "Anyone with this link can add Gaming Club folder and the 1 chat selected below." + chatCountString = "Anyone with this link can add \(title) folder and the 1 chat selected below." peersHeaderString = "1 CHAT SELECTED" } else { - chatCountString = "Anyone with this link can add Gaming Club folder and the \(state.selectedPeerIds.count) chats selected below." + chatCountString = "Anyone with this link can add \(title) folder and the \(state.selectedPeerIds.count) chats selected below." peersHeaderString = "\(state.selectedPeerIds.count) CHATS SELECTED" } entries.append(.header(chatCountString)) //TODO:localize - entries.append(.mainLinkHeader("INVITE LINK")) - entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink)) + if canShareChats { + entries.append(.mainLinkHeader("INVITE LINK")) + entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink)) + } entries.append(.peersHeader(peersHeaderString)) @@ -274,16 +278,22 @@ private func folderInviteLinkListControllerEntries( entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled)) } + if let infoString { + entries.append(.peersInfo(infoString)) + } + return entries } private struct FolderInviteLinkListControllerState: Equatable { + var title: String? var currentLink: ExportedChatFolderLink? var selectedPeerIds = Set() var generatingLink: Bool = false + var isSaving: Bool = false } -public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) -> ViewController { +public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, title filterTitle: String, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? let _ = pushControllerImpl var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? @@ -295,6 +305,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese let actionsDisposable = DisposableSet() var initialState = FolderInviteLinkListControllerState() + initialState.title = currentInvitation?.title initialState.currentLink = currentInvitation let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -313,6 +324,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese var displayTooltipImpl: ((UndoOverlayContent) -> Void)? + var didDisplayAddPeerNotice: Bool = false + let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) shareController.completed = { peerIds in @@ -368,6 +381,33 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] + + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Name Link", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + let state = stateValue.with({ $0 }) + + let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Link title", value: state.title ?? "", apply: { value in + if let value { + updateState { state in + var state = state + + state.title = value + + return state + } + } + }) + /*promptController.dismissed = { byOutsideTap in + if byOutsideTap { + completionHandler(nil) + } + }*/ + presentControllerImpl?(promptController, nil) + }))) 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) @@ -395,7 +435,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese }, action: { _, f in f(.dismissWithoutContent) - let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, revoke: true) + let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, peerIds: nil, revoke: true) |> deliverOnMainQueue).start(completed: { let _ = (context.engine.peers.revokeChatFolderLink(filterId: filterId, link: invite) |> deliverOnMainQueue).start(completed: { @@ -408,12 +448,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, peerAction: { peer, isEnabled in - let state = stateValue.with({ $0 }) - if state.currentLink != nil { - return - } - if isEnabled { + var added = false updateState { state in var state = state @@ -421,13 +457,22 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese state.selectedPeerIds.remove(peer.id) } else { state.selectedPeerIds.insert(peer.id) + added = true } return state } + + if added && !didDisplayAddPeerNotice { + didDisplayAddPeerNotice = true + + dismissTooltipsImpl?() + //TODO:localize + displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.")) + } } else { - //TODO:localized - var text = "You can't invite others here" + //TODO:localize + var text = "You can't invite others here." switch peer { case .channel: text = "You don't have the admin rights to share invite links to this group chat." @@ -519,28 +564,57 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese //TODO:localize let title: ItemListControllerTitle - title = .text("Share Folder") + + var folderTitle = "Share Folder" + if let title = state.title, !title.isEmpty { + folderTitle = title + } + title = .text(folderTitle) var doneButton: ItemListNavigationButton? - doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { - /*let state = stateValue.with({ $0 }) - if let currentLink = state.currentLink { - updateState { state in - var state = state - state.isSaving = true - return state + if state.isSaving { + doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } else { + doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let state = stateValue.with({ $0 }) + if let currentLink = state.currentLink { + if currentLink.title != state.title || Set(currentLink.peerIds) != state.selectedPeerIds { + updateState { state in + var state = state + state.isSaving = true + return state + } + actionsDisposable.add((context.engine.peers.editChatFolderLink(filterId: filterId, link: currentLink, title: state.title, peerIds: Array(state.selectedPeerIds), revoke: false) + |> deliverOnMainQueue).start(error: { _ in + updateState { state in + var state = state + state.isSaving = false + return state + } + + dismissTooltipsImpl?() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, completed: { + linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false)) + dismissImpl?() + })) + } else { + dismissImpl?() + } + } else { + dismissImpl?() } - actionsDisposable.add(context.engine.peers.editChatFolderLink(filterId: filterId, link: currentLink, title: nil, revoke: false)) - } else { dismissImpl?() - }*/ - dismissImpl?() - }) + }) + } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries( presentationData: presentationData, state: state, + title: filterTitle, allPeers: allPeers.compactMap { $0 } ), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift index ad37543a60..d3c0bbd7d3 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift @@ -34,12 +34,14 @@ public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem { let displayButton: Bool let enableButton: Bool let buttonTitle: String + let secondaryButtonTitle: String? let displayImporters: Bool let buttonColor: UIColor? public let sectionId: ItemListSectionId let style: ItemListStyle let copyAction: (() -> Void)? let shareAction: (() -> Void)? + let secondaryAction: (() -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let viewAction: (() -> Void)? public let tag: ItemListItemTag? @@ -53,12 +55,14 @@ public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem { displayButton: Bool, enableButton: Bool, buttonTitle: String, + secondaryButtonTitle: String?, displayImporters: Bool, buttonColor: UIColor?, sectionId: ItemListSectionId, style: ItemListStyle, copyAction: (() -> Void)?, shareAction: (() -> Void)?, + secondaryAction: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, viewAction: (() -> Void)?, tag: ItemListItemTag? = nil @@ -71,12 +75,14 @@ public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem { self.displayButton = displayButton self.enableButton = enableButton self.buttonTitle = buttonTitle + self.secondaryButtonTitle = secondaryButtonTitle self.displayImporters = displayImporters self.buttonColor = buttonColor self.sectionId = sectionId self.style = style self.copyAction = copyAction self.shareAction = shareAction + self.secondaryAction = secondaryAction self.contextAction = contextAction self.viewAction = viewAction self.tag = tag @@ -133,6 +139,7 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod private let addressButtonIconNode: ASImageNode private var addressShimmerNode: ShimmerEffectNode? private var shareButtonNode: SolidRoundedButtonNode? + private var secondaryButtonNode: SolidRoundedButtonNode? private let avatarsButtonNode: HighlightTrackingButtonNode private let avatarsContext: AnimatedAvatarSetContext @@ -465,13 +472,49 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod strongSelf.addSubnode(shareButtonNode) strongSelf.shareButtonNode = shareButtonNode } - shareButtonNode.title = item.buttonTitle - let buttonWidth = contentSize.width - leftInset - rightInset + if let secondaryButtonTitle = item.secondaryButtonTitle { + let secondaryButtonNode: SolidRoundedButtonNode + if let current = strongSelf.secondaryButtonNode { + secondaryButtonNode = current + } else { + let buttonTheme: SolidRoundedButtonTheme + if let buttonColor = item.buttonColor { + buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } else { + buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) + } + secondaryButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 11.0) + secondaryButtonNode.pressed = { [weak self] in + self?.item?.secondaryAction?() + } + strongSelf.addSubnode(secondaryButtonNode) + strongSelf.secondaryButtonNode = secondaryButtonNode + } + secondaryButtonNode.title = secondaryButtonTitle + } else { + if let secondaryButtonNode = strongSelf.secondaryButtonNode { + strongSelf.secondaryButtonNode = nil + secondaryButtonNode.removeFromSupernode() + } + } + + var buttonWidth = contentSize.width - leftInset - rightInset + let totalButtonWidth = buttonWidth + let buttonSpacing: CGFloat = 8.0 + if strongSelf.secondaryButtonNode != nil { + buttonWidth = floor((buttonWidth - 8.0) / 2.0) + } + let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate) shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + if let secondaryButtonNode = strongSelf.secondaryButtonNode { + let _ = secondaryButtonNode.updateLayout(width: totalButtonWidth - buttonWidth - buttonSpacing, transition: .immediate) + secondaryButtonNode.frame = CGRect(x: leftInset + buttonWidth + buttonSpacing, y: verticalInset + fieldHeight + fieldSpacing, width: totalButtonWidth - buttonWidth - buttonSpacing, height: buttonHeight) + } + var totalWidth = invitedPeersLayout.size.width var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0) let avatarSpacing: CGFloat = 21.0 diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index f8d1a08c53..3a36363166 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -338,6 +338,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let enabled: Bool let highlighted: Bool public let selectable: Bool + let highlightable: Bool let animateFirstAvatarTransition: Bool public let sectionId: ItemListSectionId let action: (() -> Void)? @@ -355,7 +356,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let displayDecorations: Bool let disableInteractiveTransitionIfNecessary: Bool - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) { + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -375,6 +376,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { self.enabled = enabled self.highlighted = highlighted self.selectable = selectable + self.highlightable = highlightable self.animateFirstAvatarTransition = animateFirstAvatarTransition self.sectionId = sectionId self.action = action @@ -1256,6 +1258,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var checkTheme = CheckNodeTheme(theme: item.presentationData.theme, style: .plain) checkTheme.isDottedBorder = !switchValue.isEnabled leftCheckNode = CheckNode(theme: checkTheme) + leftCheckNode.isUserInteractionEnabled = false strongSelf.leftCheckNode = leftCheckNode strongSelf.avatarNode.supernode?.addSubnode(leftCheckNode) } @@ -1372,7 +1375,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } strongSelf.backgroundNode.isHidden = !item.displayDecorations - strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations + strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations || !item.highlightable strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift index 7433ec6112..2289a35072 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift @@ -41,6 +41,7 @@ public enum ItemListSectionHeaderActivityIndicator { public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let text: String + let badge: String? let multiline: Bool let activityIndicator: ItemListSectionHeaderActivityIndicator let accessoryText: ItemListSectionHeaderAccessoryText? @@ -48,9 +49,10 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { public let isAlwaysPlain: Bool = true - public init(presentationData: ItemListPresentationData, text: String, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) { + public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) { self.presentationData = presentationData self.text = text + self.badge = badge self.multiline = multiline self.activityIndicator = activityIndicator self.accessoryText = accessoryText @@ -97,6 +99,8 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { private var item: ItemListSectionHeaderItem? private let titleNode: TextNode + private var badgeBackgroundLayer: SimpleLayer? + private var badgeTextNode: TextNode? private let accessoryTextNode: TextNode private var accessoryImageNode: ASImageNode? private var activityIndicator: ActivityIndicator? @@ -126,6 +130,7 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { public func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode) let previousItem = self.item @@ -135,7 +140,19 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + var badgeLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let badge = item.badge { + let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0) + badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + } + + let badgeSpacing: CGFloat = 6.0 + var textRightInset: CGFloat = 20.0 + if let badgeLayoutAndApply { + textRightInset += badgeLayoutAndApply.0.size.width + badgeSpacing + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var accessoryTextString: NSAttributedString? var accessoryIcon: UIImage? if let accessoryText = item.accessoryText { @@ -178,6 +195,43 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size) + if let badgeLayoutAndApply { + let badgeTextNode = badgeLayoutAndApply.1() + let badgeSideInset: CGFloat = 4.0 + let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0) + let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize) + + let badgeBackgroundLayer: SimpleLayer + if let current = strongSelf.badgeBackgroundLayer { + badgeBackgroundLayer = current + } else { + badgeBackgroundLayer = SimpleLayer() + strongSelf.badgeBackgroundLayer = badgeBackgroundLayer + strongSelf.layer.addSublayer(badgeBackgroundLayer) + } + + if strongSelf.badgeTextNode !== badgeTextNode { + strongSelf.badgeTextNode?.removeFromSupernode() + strongSelf.badgeTextNode = badgeTextNode + strongSelf.addSubnode(badgeTextNode) + } + + badgeBackgroundLayer.frame = badgeBackgroundFrame + badgeBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemCheckColors.fillColor.cgColor + badgeBackgroundLayer.cornerRadius = 5.0 + + badgeTextNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeLayoutAndApply.0.size.width) * 0.5), y: badgeBackgroundFrame.minY + 1.0 + floorToScreenPixels((badgeBackgroundFrame.height - badgeLayoutAndApply.0.size.height) * 0.5)), size: badgeLayoutAndApply.0.size) + } else { + if let badgeTextNode = strongSelf.badgeTextNode { + strongSelf.badgeTextNode = nil + badgeTextNode.removeFromSupernode() + } + if let badgeBackgroundLayer = strongSelf.badgeBackgroundLayer { + strongSelf.badgeBackgroundLayer = nil + badgeBackgroundLayer.removeFromSuperlayer() + } + } + var accessoryTextOffset: CGFloat = 0.0 if let accessoryIcon = accessoryIcon { accessoryTextOffset += accessoryIcon.size.width + 3.0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index d706116dea..6bb6fedce4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -3,12 +3,24 @@ import SwiftSignalKit import Postbox import TelegramApi -//communities.exportCommunityInvite#41fe69d9 community:InputCommunity title:string peers:Vector = communities.ExportedCommunityInvite; -//communities.exportedCommunityInvite#6b97a8ea filter:DialogFilter invite:ExportedCommunityInvite = communities.ExportedCommunityInvite; -//exportedCommunityInvite#af7afb2f title:string url:string peers:Vector = ExportedCommunityInvite; +public func canShareLinkToPeer(peer: EnginePeer) -> Bool { + var isEnabled = false + switch peer { + case let .channel(channel): + if channel.adminRights != nil && channel.hasPermission(.inviteMembers) { + isEnabled = true + } else if channel.username != nil { + isEnabled = true + } + default: + break + } + return isEnabled +} public enum ExportChatFolderError { case generic + case limitExceeded } public struct ExportedChatFolderLink: Equatable { @@ -47,8 +59,12 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String |> castError(ExportChatFolderError.self) |> mapToSignal { inputPeers -> Signal in return account.network.request(Api.functions.communities.exportCommunityInvite(community: .inputCommunityDialogFilter(filterId: filterId), title: title, peers: inputPeers)) - |> mapError { _ -> ExportChatFolderError in - return .generic + |> mapError { error -> ExportChatFolderError in + if error.errorDescription == "INVITES_TOO_MUCH" { + return .limitExceeded + } else { + return .generic + } } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in @@ -237,6 +253,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal Signal Signal Signal { @@ -300,8 +334,12 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi |> castError(JoinChatFolderLinkError.self) |> mapToSignal { inputPeers -> Signal in return account.network.request(Api.functions.communities.joinCommunityInvite(slug: slug, peers: inputPeers)) - |> mapError { _ -> JoinChatFolderLinkError in - return .generic + |> mapError { error -> JoinChatFolderLinkError in + if error.errorDescription == "DIALOG_FILTERS_TOO_MUCH" { + return .limitExceeded + } else { + return .generic + } } |> mapToSignal { result -> Signal in account.stateManager.addUpdates(result) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 9965bd5061..2c9b0223c8 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/CheckNode", "//submodules/Markdown", "//submodules/UndoUI", + "//submodules/PremiumUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift index 6055b622c4..c3f97c1ef4 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift @@ -7,6 +7,81 @@ import AccountContext import MultilineTextComponent import TelegramPresentationData +final class BadgeComponent: Component { + let fillColor: UIColor + let content: AnyComponent + + init( + fillColor: UIColor, + content: AnyComponent + ) { + self.fillColor = fillColor + self.content = content + } + + static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.fillColor != rhs.fillColor { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + final class View: UIView { + private let backgroundLayer: SimpleLayer + private let content = ComponentView() + + override init(frame: CGRect) { + self.backgroundLayer = SimpleLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let height: CGFloat = 20.0 + let contentInset: CGFloat = 10.0 + + let contentSize = self.content.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: availableSize + ) + let backgroundWidth: CGFloat = max(height, contentSize.width + contentInset) + let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundWidth, height: height)) + + transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) + transition.setBackgroundColor(layer: self.backgroundLayer, color: component.fillColor) + transition.setCornerRadius(layer: self.backgroundLayer, cornerRadius: height / 2.0) + + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize)) + } + + return backgroundFrame.size + } + } + + 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) + } +} + final class ChatFolderLinkHeaderComponent: Component { let theme: PresentationTheme let strings: PresentationStrings @@ -47,6 +122,8 @@ final class ChatFolderLinkHeaderComponent: Component { private let title = ComponentView() private let separatorLayer = SimpleLayer() + private var badge: ComponentView? + private var component: ChatFolderLinkHeaderComponent? override init(frame: CGRect) { @@ -71,6 +148,7 @@ final class ChatFolderLinkHeaderComponent: Component { let height: CGFloat = 60.0 let spacing: CGFloat = 16.0 + let badgeSpacing: CGFloat = 6.0 if themeUpdated { //TODO:localize @@ -143,6 +221,39 @@ final class ChatFolderLinkHeaderComponent: Component { ) contentWidth += titleSize.width + var badgeSize: CGSize? + if let badge = component.badge { + let badgeContainer: ComponentView + var badgeTransition = transition + if let current = self.badge { + badgeContainer = current + } else { + badgeTransition = .immediate + badgeContainer = ComponentView() + self.badge = badgeContainer + } + let badgeSizeValue = badgeContainer.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + fillColor: component.theme.list.itemCheckColors.fillColor, + content: AnyComponent(Text(text: badge, font: Font.semibold(12.0), color: component.theme.list.itemCheckColors.foregroundColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + badgeSize = badgeSizeValue + contentWidth += badgeSpacing + badgeSizeValue.width + } else { + if let badge = self.badge { + self.badge = nil + if let view = badge.view { + transition.setScale(view: view, scale: 0.0001, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + } + } + contentWidth += spacing if let rightImage = self.rightView.image { contentWidth += rightImage.size.width @@ -163,9 +274,35 @@ final class ChatFolderLinkHeaderComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - titleSize.height) / 2.0)), size: titleSize) transition.setFrame(view: titleView, frame: titleFrame) - transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + 9.0), size: CGSize(width: titleFrame.width, height: 3.0))) + var separatorWidth = titleFrame.width + + if let badgeSize, let badge = self.badge { + let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + 1.0 + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize) + + separatorWidth += badgeSpacing + badgeSize.width + + if let badgeView = badge.view { + var badgeTransition = transition + var animateIn = false + if badgeView.superview == nil { + badgeTransition = .immediate + self.addSubview(badgeView) + animateIn = true + } + badgeTransition.setFrame(view: badgeView, frame: badgeFrame) + if animateIn { + transition.animateScale(view: badgeView, from: 0.0001, to: 1.0) + } + } + } + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + 9.0), size: CGSize(width: separatorWidth, height: 3.0))) } contentOriginX += titleSize.width + if let badgeSize { + contentOriginX += badgeSpacing + contentOriginX += badgeSize.width + } contentOriginX += spacing if let rightImage = self.rightView.image { diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 7caf63e270..9f26f8d775 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -15,6 +15,7 @@ import SolidRoundedButtonComponent import PresentationDataUtils import Markdown import UndoUI +import PremiumUI private final class ChatFolderLinkPreviewScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -66,6 +67,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } } + final class AnimationHint { + init() { + } + } + final class View: UIView, UIScrollViewDelegate { private let dimView: UIView private let backgroundLayer: SimpleLayer @@ -82,6 +88,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { private let actionButton = ComponentView() private let listHeaderText = ComponentView() + private let listHeaderAction = ComponentView() private let itemContainerView: UIView private var items: [AnyHashable: ComponentView] = [:] @@ -240,7 +247,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.backgroundLayer.frame.minY + if let controller = self.environment?.controller() { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY 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 @@ -254,6 +265,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let animationHint = transition.userData(AnimationHint.self) + + var contentTransition = transition + if animationHint != nil { + contentTransition = .immediate + } + let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -282,7 +300,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { var contentHeight: CGFloat = 0.0 let leftButtonSize = self.leftButton.update( - transition: transition, + transition: contentTransition, component: AnyComponent(Button( content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), action: { [weak self] in @@ -331,19 +349,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) } - transition.setFrame(view: titleView, frame: titleFrame) + contentTransition.setFrame(view: titleView, frame: titleFrame) } contentHeight += 44.0 contentHeight += 14.0 + var topBadge: String? + if let linkContents = component.linkContents, linkContents.localFilterId != nil { + topBadge = "+\(linkContents.peers.count)" + } + let topIconSize = self.topIcon.update( - transition: transition, + transition: contentTransition, component: AnyComponent(ChatFolderLinkHeaderComponent( theme: environment.theme, strings: environment.strings, title: component.linkContents?.title ?? "Folder", - badge: nil + badge: topBadge )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) @@ -353,7 +376,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { if topIconView.superview == nil { self.scrollContentView.addSubview(topIconView) } - transition.setFrame(view: topIconView, frame: topIconFrame) + contentTransition.setFrame(view: topIconView, frame: topIconFrame) topIconView.isHidden = component.linkContents == nil } @@ -404,7 +427,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { if descriptionTextView.superview == nil { self.scrollContentView.addSubview(descriptionTextView) } - transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + contentTransition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) } contentHeight += descriptionTextFrame.height @@ -446,15 +469,27 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { 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 else { + guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { return } - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) + + 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 { - self.selectedItems.insert(peer.id) + 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))) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) } )), environment: {}, @@ -493,7 +528,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { listHeaderTitle = "\(self.selectedItems.count) CHATS IN FOLDER TO JOIN" } + let listHeaderActionTitle: String + if self.selectedItems.count == self.items.count { + listHeaderActionTitle = "DESELECT ALL" + } else { + listHeaderActionTitle = "SELECT ALL" + } + 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, @@ -517,14 +560,60 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.scrollContentView.addSubview(listHeaderTextView) } let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize) - transition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin) + contentTransition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin) listHeaderTextView.bounds = CGRect(origin: CGPoint(), size: listHeaderTextFrame.size) listHeaderTextView.isHidden = component.linkContents == nil } + + 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 } + ) + ) + )), + action: { [weak self] in + guard let self, let component = self.component, let linkContents = component.linkContents else { + return + } + if self.selectedItems.count != linkContents.peers.count { + for peer in linkContents.peers { + self.selectedItems.insert(peer.id) + } + } else { + self.selectedItems.removeAll() + for peerId in linkContents.alreadyMemberPeerIds { + self.selectedItems.insert(peerId) + } + } + self.state?.updated(transition: .immediate) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0) + ) + if let listHeaderActionView = self.listHeaderAction.view { + if listHeaderActionView.superview == nil { + listHeaderActionView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + 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) + listHeaderActionView.isHidden = component.linkContents == nil + } + contentHeight += listHeaderTextSize.height contentHeight += 6.0 - transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) + 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)) @@ -536,7 +625,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let actionButtonTitle: String if let linkContents = component.linkContents { if linkContents.localFilterId != nil { - actionButtonTitle = "Join Chats" + if self.selectedItems.isEmpty { + actionButtonTitle = "Do Not Join Any Chats" + } else { + actionButtonTitle = "Join Chats" + } } else { actionButtonTitle = "Add Folder" } @@ -555,25 +648,41 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { height: 50.0, cornerRadius: 11.0, gloss: false, - isEnabled: !self.selectedItems.isEmpty, + isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, animationName: nil, iconPosition: .right, iconSpacing: 4.0, isLoading: component.linkContents == nil, action: { [weak self] in - guard let self, let component = self.component else { + guard let self, let component = self.component, let controller = self.environment?.controller() else { return } if let _ = component.linkContents { if self.joinDisposable == nil, !self.selectedItems.isEmpty { self.joinDisposable = (component.context.engine.peers.joinChatFolderLink(slug: component.slug, peerIds: Array(self.selectedItems)) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).start(error: { [weak self] error in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + switch error { + case .generic: + controller.dismiss() + case .limitExceeded: + //TODO:localize + let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: 5, action: {}) + controller.push(limitController) + controller.dismiss() + } + }, completed: { [weak self] in guard let self, let controller = self.environment?.controller() else { return } controller.dismiss() }) + } else { + controller.dismiss() } } @@ -613,6 +722,19 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } + if let controller = environment.controller() { + let subLayout = ContainerViewLayout( + size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset), + safeInsets: UIEdgeInsets(), + additionalInsets: UIEdgeInsets(), + statusBarHeight: nil, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition) + } + contentHeight += bottomPanelHeight initialContentHeight += bottomPanelHeight @@ -676,14 +798,23 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { self.blocksBackgroundWhenInOverlay = true self.linkContentsDisposable = (context.engine.peers.checkChatFolderLink(slug: slug) - //|> delay(0.2, queue: .mainQueue()) + |> delay(0.2, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } self.linkContents = result - self.updateComponent(component: AnyComponent(ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: result)), transition: .immediate) + self.updateComponent(component: AnyComponent(ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: result)), transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)).withUserData(ChatFolderLinkPreviewScreenComponent.AnimationHint())) + }, error: { [weak self] _ in + guard let self else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "The folder link has expired."), elevatedLayout: false, action: { _ in true }), in: .window(.root)) + self.dismiss() }) + + self.automaticallyControlPresentationContextLayout = false } required public init(coder aDecoder: NSCoder) { @@ -694,6 +825,10 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { self.linkContentsDisposable?.dispose() } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated)