diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2e11e8fa8b..19c6450e87 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9102,3 +9102,16 @@ Sorry for the inconvenience."; "Wallpaper.ApplyForAll" = "Apply For All Chats"; "Wallpaper.ApplyForChat" = "Apply For This Chat"; + +"ChatList.ChatFolderUpdateCount_1" = "1 new chat"; +"ChatList.ChatFolderUpdateCount_any" = "%d new chats"; +"ChatList.ChatFolderUpdateHintTitle" = "You can join %@"; +"ChatList.ChatFolderUpdateHintText" = "Tap here to view them"; + +"Premium.MaxSharedFolderMembershipText" = "You can only add **%1$@** shareable folders. Upgrade to **Telegram Premium** to increase this limit up to **%2$@**."; +"Premium.MaxSharedFolderMembershipNoPremiumText" = "You can only add **%1$@** shareable folders. We are working to let you increase this limit in the future."; +"Premium.MaxSharedFolderMembershipFinalText" = "Sorry, you can only add **%1$@** shareable folders."; + +"Premium.MaxSharedFolderLinksText" = "You can only create **%1$@** invite links. Upgrade to **Telegram Premium** to increase the links limit to **%2$@**."; +"Premium.MaxSharedFolderLinksNoPremiumText" = "You can only create **%1$@** invite links. We are working to let you increase this limit in the future."; +"Premium.MaxSharedFolderLinksFinalText" = "Sorry, you can only create **%1$@** invite links"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0484642b5d..a63259e0ab 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -896,6 +896,8 @@ public enum PremiumLimitSubject { case pins case files case accounts + case linksPerSharedFolder + case membershipInSharedFolders } public protocol ComposeController: ViewController { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 45d5181b52..251c124056 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -89,6 +89,7 @@ swift_library( "//submodules/AvatarNode:AvatarNode", "//submodules/AvatarVideoNode:AvatarVideoNode", "//submodules/InviteLinksUI", + "//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5eacbd56e9..4b48549191 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -44,6 +44,7 @@ import ComponentDisplayAdapters import ChatListHeaderComponent import ChatListTitleView import InviteLinksUI +import ChatFolderLinkPreviewScreen private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -1544,7 +1545,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 +1554,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,56 +2700,122 @@ 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) { - let actionSheet = ActionSheetController(presentationData: self.presentationData) + let apply: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + let commit: () -> Void = { + guard let strongSelf = self else { + return + } + + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing { + strongSelf.donePressed() + } + } + + let _ = (strongSelf.context.engine.peers.updateChatListFiltersInteractively { filters in + return filters.filter({ $0.id != id }) + }).start() + } + + if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { + strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: .all, completion: { + commit() + }) + } else { + commit() + } + } - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: self.presentationData.strings.ChatList_RemoveFolderConfirmation), - ActionSheetButtonItem(title: self.presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { + let _ = (self.context.engine.peers.currentChatListFilters() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] filters in + guard let self else { + return + } + guard let filter = filters.first(where: { $0.id == id }) else { + return + } + + if case let .filter(_, title, _, data) = filter, data.isShared { + let _ = (self.context.engine.data.get( + EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self else { return } - let commit: () -> Void = { - guard let strongSelf = self else { - return - } - - if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { - if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing { - strongSelf.donePressed() - } - } - - let _ = (strongSelf.context.engine.peers.updateChatListFiltersInteractively { filters in - return filters.filter({ $0.id != id }) - }).start() - } + let presentationData = self.presentationData - if strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { - strongSelf.chatListDisplayNode.mainContainerNode.switchToFilter(id: .all, completion: { - commit() + //TODO:localize + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Folder", text: "Are you sure you want to delete this folder? This will also deactivate all the invite links used to share this folder.", actions: [ + TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { [weak self] in + guard let self else { + return + } + + let previewScreen = ChatFolderLinkPreviewScreen( + context: self.context, + subject: .remove(folderId: id), + contents: ChatFolderLinkContents( + localFilterId: id, + title: title, + peers: peers.compactMap { $0 }.filter { peer in + if case .channel = peer { + return true + } else { + return false + } + }, + alreadyMemberPeerIds: Set() + ), + completion: { [weak self] in + guard let self else { + return + } + if self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id == id { + self.chatListDisplayNode.mainContainerNode.switchToFilter(id: .all, completion: { + }) + } + } + ) + self.push(previewScreen) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { }) - } else { - commit() - } + ]), in: .window(.root)) }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.present(actionSheet, in: .window(.root)) + } else { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: self.presentationData.strings.ChatList_RemoveFolderConfirmation), + ActionSheetButtonItem(title: self.presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + apply() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.present(actionSheet, in: .window(.root)) + } + }) } public private(set) var isSearchActive: Bool = false diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index e208322ea6..09a57770ce 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -186,7 +186,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index d78e95e6a1..c288034a71 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,87 @@ 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 let .limitExceeded(limit, premiumLimit): + if limit < premiumLimit { + let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: { + }) + pushControllerImpl?(limitController) + + return + } + 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 +1459,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/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index b1f36d3720..2ae9924667 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -12,6 +12,7 @@ import ItemListPeerActionItem import ChatListFilterSettingsHeaderItem import PremiumUI import UndoUI +import ChatFolderLinkPreviewScreen private final class ChatListFilterPresetListControllerArguments { let context: AccountContext @@ -380,32 +381,75 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch return state } }, removePreset: { id in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.ChatList_RemoveFolderConfirmation), - ActionSheetButtonItem(title: presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + let _ = (context.engine.peers.currentChatListFilters() + |> take(1) + |> deliverOnMainQueue).start(next: { filters in + guard let filter = filters.first(where: { $0.id == id }) else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + if case let .filter(_, title, _, data) = filter, data.isShared { + let _ = (context.engine.data.get( + EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { peers in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - if let index = filters.firstIndex(where: { $0.id == id }) { - filters.remove(at: index) - } - return filters - } - |> deliverOnMainQueue).start() + //TODO:localize + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Folder", text: "Are you sure you want to delete this folder? This will also deactivate all the invite links used to share this folder.", actions: [ + TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { + let previewScreen = ChatFolderLinkPreviewScreen( + context: context, + subject: .remove(folderId: id), + contents: ChatFolderLinkContents( + localFilterId: id, + title: title, + peers: peers.compactMap { $0 }.filter { peer in + if case .channel = peer { + return true + } else { + return false + } + }, + alreadyMemberPeerIds: Set() + ) + ) + pushControllerImpl?(previewScreen) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + }) + ])) }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - presentControllerImpl?(actionSheet) + } else { + let actionSheet = ActionSheetController(presentationData: presentationData) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.ChatList_RemoveFolderConfirmation), + ActionSheetButtonItem(title: presentationData.strings.ChatList_RemoveFolderAction, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + if let index = filters.firstIndex(where: { $0.id == id }) { + filters.remove(at: index) + } + return filters + } + |> deliverOnMainQueue).start() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + presentControllerImpl?(actionSheet) + } + }) }) let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index efecbd9832..8748e74d1e 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -125,6 +125,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN private let titleNode: TextNode private let labelNode: TextNode private let arrowNode: ASImageNode + private let sharedIconNode: ASImageNode private let activateArea: AccessibilityAreaNode @@ -169,6 +170,11 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN self.arrowNode.displaysAsynchronously = false self.arrowNode.isLayerBacked = true + self.sharedIconNode = ASImageNode() + self.sharedIconNode.displayWithoutProcessing = true + self.sharedIconNode.displaysAsynchronously = false + self.sharedIconNode.isLayerBacked = true + self.activateArea = AccessibilityAreaNode() self.highlightedBackgroundNode = ASDisplayNode() @@ -180,6 +186,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN self.containerNode.addSubnode(self.titleNode) self.containerNode.addSubnode(self.labelNode) self.containerNode.addSubnode(self.arrowNode) + self.containerNode.addSubnode(self.sharedIconNode) self.addSubnode(self.activateArea) self.activateArea.activate = { [weak self] in @@ -199,6 +206,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN return { item, params, neighbors in var updatedTheme: PresentationTheme? var updateArrowImage: UIImage? + var updatedSharedIconImage: UIImage? if currentItem?.presentationData.theme !== item.presentationData.theme || currentItem?.isDisabled != item.isDisabled { updatedTheme = item.presentationData.theme @@ -207,6 +215,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } else { updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) } + updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Share"), color: item.presentationData.theme.list.disclosureArrowColor) } let peerRevealOptions: [ItemListRevealOption] @@ -379,10 +388,14 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) transition.updateAlpha(node: strongSelf.arrowNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) + transition.updateAlpha(node: strongSelf.sharedIconNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) if let updateArrowImage = updateArrowImage { strongSelf.arrowNode.image = updateArrowImage } + if let updatedSharedIconImage { + strongSelf.sharedIconNode.image = updatedSharedIconImage + } if let arrowImage = strongSelf.arrowNode.image { var rightArrowInset = 0.0 @@ -393,6 +406,15 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } strongSelf.arrowNode.isHidden = item.isAllChats + if let sharedIconImage = strongSelf.sharedIconNode.image { + strongSelf.sharedIconNode.frame = CGRect(origin: CGPoint(x: strongSelf.arrowNode.frame.minX + 2.0 - sharedIconImage.size.width, y: floorToScreenPixels((layout.contentSize.height - sharedIconImage.size.height) / 2.0) + 1.0), size: sharedIconImage.size) + } + var isShared = false + if case let .filter(_, _, _, data) = item.preset, data.isShared { + isShared = true + } + strongSelf.sharedIconNode.isHidden = !isShared + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) @@ -483,6 +505,10 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN var arrowFrame = self.arrowNode.frame arrowFrame.origin.x = params.width - params.rightInset - 7.0 - arrowFrame.width + revealOffset transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + + var sharedIconFrame = self.sharedIconNode.frame + sharedIconFrame.origin.x = arrowFrame.minX + 2.0 - sharedIconFrame.width + transition.updateFrame(node: self.sharedIconNode, frame: sharedIconFrame) } override func revealOptionsInteractivelyOpened() { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index ca4c846ae7..4833ad97f4 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2164,6 +2164,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openChatFolderUpdates: { }) chatListInteraction.isSearchMode = true @@ -3397,7 +3398,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) var isInlineMode = false if case .topics = key { isInlineMode = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 606fcb6533..c897c5e2b0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -17,6 +17,7 @@ import PremiumUI import AnimationCache import MultiAnimationRenderer import Postbox +import ChatFolderLinkPreviewScreen public enum ChatListNodeMode { case chatList @@ -95,6 +96,7 @@ public final class ChatListNodeInteraction { let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void + let openChatFolderUpdates: () -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -139,7 +141,8 @@ public final class ChatListNodeInteraction { openForumThread: @escaping (EnginePeer.Id, Int64) -> Void, openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, - openPremiumIntro: @escaping () -> Void + openPremiumIntro: @escaping () -> Void, + openChatFolderUpdates: @escaping () -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -172,6 +175,7 @@ public final class ChatListNodeInteraction { self.openStorageManagement = openStorageManagement self.openPasswordSetup = openPasswordSetup self.openPremiumIntro = openPremiumIntro + self.openChatFolderUpdates = openChatFolderUpdates } } @@ -620,6 +624,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount: nodeInteraction?.openPremiumIntro() + case .chatFolderUpdates: + nodeInteraction?.openChatFolderUpdates() } }), directionHint: entry.directionHint) } @@ -873,6 +879,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount: nodeInteraction?.openPremiumIntro() + case .chatFolderUpdates: + nodeInteraction?.openChatFolderUpdates() } }), directionHint: entry.directionHint) case .HeaderEntry: @@ -1068,6 +1076,9 @@ public final class ChatListNode: ListView { let hideArhiveIntro = ValuePromise(false, ignoreRepeated: true) + private let chatFolderUpdates = Promise(nil) + private var pollFilterUpdatesDisposable: Disposable? + public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) { self.context = context self.location = location @@ -1389,6 +1400,19 @@ public final class ChatListNode: ListView { } let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads) self.push?(controller) + }, openChatFolderUpdates: { [weak self] in + guard let self else { + return + } + let _ = (self.chatFolderUpdates.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let result else { + return + } + + self.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(result), contents: result.chatFolderLinkContents)) + }) }) nodeInteraction.isInlineMode = isInlineMode @@ -1614,15 +1638,18 @@ public final class ChatListNode: ListView { suggestedChatListNotice, savedMessagesPeer, chatListViewUpdate, + self.chatFolderUpdates.get() |> distinctUntilChanged, self.statePromise.get() ) - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state) -> Signal in + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) let notice: ChatListNotice? - if let suggestedChatListNotice { + if let chatFolderUpdates, chatFolderUpdates.availableChatsToJoin != 0 { + notice = .chatFolderUpdates(count: chatFolderUpdates.availableChatsToJoin) + } else if let suggestedChatListNotice { notice = suggestedChatListNotice } else if let storageInfo { notice = .clearStorage(sizeFraction: storageInfo) @@ -2551,6 +2578,7 @@ public final class ChatListNode: ListView { } } + self.pollFilterUpdates(shouldDelay: false) self.resetFilter() let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) @@ -2567,6 +2595,7 @@ public final class ChatListNode: ListView { self.chatListDisposable.dispose() self.activityStatusesDisposable?.dispose() self.updatedFilterDisposable.dispose() + self.pollFilterUpdatesDisposable?.dispose() } func updateFilter(_ filter: ChatListFilter?) { @@ -2576,6 +2605,20 @@ public final class ChatListNode: ListView { } } + private func pollFilterUpdates(shouldDelay: Bool) { + guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else { + return + } + self.pollFilterUpdatesDisposable = (context.engine.peers.getChatFolderUpdates(folderId: id) + |> delay(shouldDelay ? 5.0 : 0.0, queue: .mainQueue())).start(next: { [weak self] result in + guard let self else { + return + } + self.chatFolderUpdates.set(.single(result)) + self.pollFilterUpdates(shouldDelay: true) + }) + } + private func resetFilter() { if let chatListFilter = self.chatListFilter { self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters() diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 7cfd4f5ad9..8d9b8d8300 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -51,6 +51,7 @@ enum ChatListNotice: Equatable { case setupPassword case premiumUpgrade(discount: Int32) case premiumAnnualDiscount(discount: Int32) + case chatFolderUpdates(count: Int) } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 52082e38b3..98a51e5904 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -160,6 +160,15 @@ class ChatListStorageInfoItemNode: ListViewItemNode { titleString = titleStringValue textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + case let .chatFolderUpdates(count): + let rawTitleString = item.strings.ChatList_ChatFolderUpdateHintTitle(item.strings.ChatList_ChatFolderUpdateCount(Int32(count))) + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + if let range = rawTitleString.ranges.first { + titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range) + } + titleString = titleStringValue + + textString = NSAttributedString(string: item.strings.ChatList_ChatFolderUpdateHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 6a3f513e52..cdc6081021 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -94,6 +94,7 @@ public final class HashtagSearchController: TelegramBaseController { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openChatFolderUpdates: { }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) 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..401441ab78 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,27 +278,35 @@ 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)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? + var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)? var dismissTooltipsImpl: (() -> Void)? 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 +325,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 +382,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,9 +436,9 @@ 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) + let _ = (context.engine.peers.deleteChatFolderLink(filterId: filterId, link: invite) |> deliverOnMainQueue).start(completed: { linkUpdated(nil) dismissImpl?() @@ -408,12 +449,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 +458,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." @@ -483,6 +529,40 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese EngineDataList(combinedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) ) + let applyChangesImpl: (() -> Void)? = { + 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?() + } + dismissImpl?() + } + let _ = (allPeers |> take(1) |> deliverOnMainQueue).start(next: { peers in @@ -519,28 +599,27 @@ 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 - } - actionsDisposable.add(context.engine.peers.editChatFolderLink(filterId: filterId, link: currentLink, title: nil, revoke: false)) - } else { - dismissImpl?() - }*/ - dismissImpl?() - }) + 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: { + applyChangesImpl?() + }) + } 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) @@ -563,6 +642,43 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } + controller.attemptNavigation = { f in + return attemptNavigationImpl?(f) ?? true + } + attemptNavigationImpl = { f in + if let currentInvitation { + let state = stateValue.with({ $0 }) + + var hasChanges = false + if state.title != currentInvitation.title { + hasChanges = true + } + if state.selectedPeerIds != Set(currentInvitation.peerIds) { + hasChanges = true + } + + if hasChanges { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + //TODO:localize + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Unsaved Changes", text: "You have changed the settings of this folder. Apply changes?", actions: [ + TextAlertAction(type: .genericAction, title: "Discard", action: { + f() + dismissImpl?() + }), + TextAlertAction(type: .defaultAction, title: "Apply", action: { + applyChangesImpl?() + }) + ]), nil) + return false + } else { + f() + return true + } + } else { + f() + return true + } + } pushControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) 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 fce31ab2f2..1aedfdac77 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) } @@ -1377,7 +1380,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/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index d19d22056d..4fbb8215e0 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -172,6 +172,18 @@ public enum PremiumSource: Equatable { } else { return false } + case .linksPerSharedFolder: + if case .linksPerSharedFolder = rhs { + return true + } else { + return false + } + case .membershipInSharedFolders: + if case .membershipInSharedFolders = rhs { + return true + } else { + return false + } } } @@ -199,6 +211,8 @@ public enum PremiumSource: Equatable { case voiceToText case fasterDownload case translation + case linksPerSharedFolder + case membershipInSharedFolders var identifier: String? { switch self { @@ -252,6 +266,10 @@ public enum PremiumSource: Equatable { } case .translation: return "translations" + case .linksPerSharedFolder: + return "double_limits__community_invites" + case .membershipInSharedFolders: + return "double_limits__communities_joined" } } } diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 5e9910a260..91be1ea6d4 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -765,6 +765,36 @@ private final class LimitSheetContent: CombinedComponent { badgeText = "\(limit)" string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string } + case .linksPerSharedFolder: + //TODO:localize + let limit = state.limits.maxSharedFolderInviteLinks + let premiumLimit = state.premiumLimits.maxSharedFolderInviteLinks + iconName = "Premium/Link" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string + } + case .membershipInSharedFolders: + //TODO:localize + let limit = state.limits.maxSharedFolderJoin + let premiumLimit = state.premiumLimits.maxSharedFolderJoin + iconName = "Premium/Folder" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxSharedFolderMembershipNoPremiumText("\(limit)").string + } case .pins: let limit = state.limits.maxPinnedChatCount let premiumLimit = state.premiumLimits.maxPinnedChatCount @@ -1048,6 +1078,8 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case pins case files case accounts + case linksPerSharedFolder + case membershipInSharedFolders } public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) { diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 046b9e9e6d..89a298c377 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 1fdc57b25c..13d5670d84 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -843,7 +843,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}) + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) func makeChatListItem( diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 35b0762523..2bd110fa32 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -367,7 +367,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {} ) + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) func makeChatListItem( peer: EnginePeer, diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ad62af54f8..056930ae34 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -5,7 +5,6 @@ public enum Api { public enum bots {} public enum channels {} public enum communities {} - public enum community {} public enum contacts {} public enum help {} public enum messages {} @@ -998,11 +997,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } dict[-266911767] = { return Api.channels.ChannelParticipants.parse_channelParticipantsNotModified($0) } dict[-191450938] = { return Api.channels.SendAsPeers.parse_sendAsPeers($0) } + dict[59080097] = { return Api.communities.CommunityInvite.parse_communityInvite($0) } + dict[-951718393] = { return Api.communities.CommunityInvite.parse_communityInviteAlready($0) } dict[-414818125] = { return Api.communities.CommunityUpdates.parse_communityUpdates($0) } dict[1805101290] = { return Api.communities.ExportedCommunityInvite.parse_exportedCommunityInvite($0) } dict[-2662489] = { return Api.communities.ExportedInvites.parse_exportedInvites($0) } - dict[988463765] = { return Api.community.CommunityInvite.parse_communityInvite($0) } - dict[74184410] = { return Api.community.CommunityInvite.parse_communityInviteAlready($0) } dict[182326673] = { return Api.contacts.Blocked.parse_blocked($0) } dict[-513392236] = { return Api.contacts.Blocked.parse_blockedSlice($0) } dict[-353862078] = { return Api.contacts.Contacts.parse_contacts($0) } @@ -1802,14 +1801,14 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.channels.SendAsPeers: _1.serialize(buffer, boxed) + case let _1 as Api.communities.CommunityInvite: + _1.serialize(buffer, boxed) case let _1 as Api.communities.CommunityUpdates: _1.serialize(buffer, boxed) case let _1 as Api.communities.ExportedCommunityInvite: _1.serialize(buffer, boxed) case let _1 as Api.communities.ExportedInvites: _1.serialize(buffer, boxed) - case let _1 as Api.community.CommunityInvite: - _1.serialize(buffer, boxed) case let _1 as Api.contacts.Blocked: _1.serialize(buffer, boxed) case let _1 as Api.contacts.Contacts: diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index dc8bb6d0e8..45727fa53f 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -894,6 +894,140 @@ public extension Api.channels { } } +public extension Api.communities { + enum CommunityInvite: TypeConstructorDescription { + case communityInvite(flags: Int32, title: String, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + case communityInviteAlready(filterId: Int32, missingPeers: [Api.Peer], alreadyPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .communityInvite(let flags, let title, let emoticon, let peers, let chats, let users): + if boxed { + buffer.appendInt32(59080097) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .communityInviteAlready(let filterId, let missingPeers, let alreadyPeers, let chats, let users): + if boxed { + buffer.appendInt32(-951718393) + } + serializeInt32(filterId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(missingPeers.count)) + for item in missingPeers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(alreadyPeers.count)) + for item in alreadyPeers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .communityInvite(let flags, let title, let emoticon, let peers, let chats, let users): + return ("communityInvite", [("flags", flags as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) + case .communityInviteAlready(let filterId, let missingPeers, let alreadyPeers, let chats, let users): + return ("communityInviteAlready", [("filterId", filterId as Any), ("missingPeers", missingPeers as Any), ("alreadyPeers", alreadyPeers as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_communityInvite(_ reader: BufferReader) -> CommunityInvite? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: [Api.Peer]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.communities.CommunityInvite.communityInvite(flags: _1!, title: _2!, emoticon: _3, peers: _4!, chats: _5!, users: _6!) + } + else { + return nil + } + } + public static func parse_communityInviteAlready(_ reader: BufferReader) -> CommunityInvite? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Peer]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _3: [Api.Peer]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.communities.CommunityInvite.communityInviteAlready(filterId: _1!, missingPeers: _2!, alreadyPeers: _3!, chats: _4!, users: _5!) + } + else { + return nil + } + } + + } +} public extension Api.communities { enum CommunityUpdates: TypeConstructorDescription { case communityUpdates(missingPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) @@ -1000,181 +1134,3 @@ public extension Api.communities { } } -public extension Api.communities { - enum ExportedInvites: TypeConstructorDescription { - case exportedInvites(invites: [Api.ExportedCommunityInvite], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedInvites(let invites, let chats, let users): - if boxed { - buffer.appendInt32(-2662489) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(invites.count)) - for item in invites { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedInvites(let invites, let chats, let users): - return ("exportedInvites", [("invites", invites as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_exportedInvites(_ reader: BufferReader) -> ExportedInvites? { - var _1: [Api.ExportedCommunityInvite]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ExportedCommunityInvite.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.communities.ExportedInvites.exportedInvites(invites: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.community { - enum CommunityInvite: TypeConstructorDescription { - case communityInvite(title: String, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) - case communityInviteAlready(filterId: Int32, missingPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .communityInvite(let title, let peers, let chats, let users): - if boxed { - buffer.appendInt32(988463765) - } - serializeString(title, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(peers.count)) - for item in peers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .communityInviteAlready(let filterId, let missingPeers, let chats, let users): - if boxed { - buffer.appendInt32(74184410) - } - serializeInt32(filterId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(missingPeers.count)) - for item in missingPeers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .communityInvite(let title, let peers, let chats, let users): - return ("communityInvite", [("title", title as Any), ("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) - case .communityInviteAlready(let filterId, let missingPeers, let chats, let users): - return ("communityInviteAlready", [("filterId", filterId as Any), ("missingPeers", missingPeers as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_communityInvite(_ reader: BufferReader) -> CommunityInvite? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.Peer]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.community.CommunityInvite.communityInvite(title: _1!, peers: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - public static func parse_communityInviteAlready(_ reader: BufferReader) -> CommunityInvite? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Peer]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.community.CommunityInvite.communityInviteAlready(filterId: _1!, missingPeers: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 804137fbf1..5e7072c318 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1,3 +1,65 @@ +public extension Api.communities { + enum ExportedInvites: TypeConstructorDescription { + case exportedInvites(invites: [Api.ExportedCommunityInvite], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedInvites(let invites, let chats, let users): + if boxed { + buffer.appendInt32(-2662489) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(invites.count)) + for item in invites { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedInvites(let invites, let chats, let users): + return ("exportedInvites", [("invites", invites as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_exportedInvites(_ reader: BufferReader) -> ExportedInvites? { + var _1: [Api.ExportedCommunityInvite]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ExportedCommunityInvite.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.communities.ExportedInvites.exportedInvites(invites: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.contacts { enum Blocked: TypeConstructorDescription { case blocked(blocked: [Api.PeerBlocked], chats: [Api.Chat], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 82cf5432cd..89d78efab9 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -2953,15 +2953,15 @@ public extension Api.functions.channels { } } public extension Api.functions.communities { - static func checkCommunityInvite(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func checkCommunityInvite(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1753956947) + buffer.appendInt32(161196517) serializeString(slug, buffer: buffer, boxed: false) - return (FunctionDescription(name: "communities.checkCommunityInvite", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.community.CommunityInvite? in + return (FunctionDescription(name: "communities.checkCommunityInvite", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.communities.CommunityInvite? in let reader = BufferReader(buffer) - var result: Api.community.CommunityInvite? + var result: Api.communities.CommunityInvite? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.community.CommunityInvite + result = Api.parse(reader, signature: signature) as? Api.communities.CommunityInvite } return result }) @@ -3092,6 +3092,46 @@ public extension Api.functions.communities { }) } } +public extension Api.functions.communities { + static func joinCommunityUpdates(community: Api.InputCommunity, peers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1372856854) + community.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "communities.joinCommunityUpdates", parameters: [("community", String(describing: community)), ("peers", String(describing: peers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} +public extension Api.functions.communities { + static func leaveCommunity(community: Api.InputCommunity, peers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(903443807) + community.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "communities.leaveCommunity", parameters: [("community", String(describing: community)), ("peers", String(describing: peers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.contacts { static func acceptContact(id: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index f81eb629ef..cb77e07285 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -15,6 +15,8 @@ public struct UserLimitsConfiguration: Equatable { public let maxAboutLength: Int32 public let maxAnimatedEmojisInText: Int32 public let maxReactionsPerMessage: Int32 + public let maxSharedFolderInviteLinks: Int32 + public let maxSharedFolderJoin: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -30,7 +32,9 @@ public struct UserLimitsConfiguration: Equatable { maxUploadFileParts: 4000, maxAboutLength: 70, maxAnimatedEmojisInText: 10, - maxReactionsPerMessage: 1 + maxReactionsPerMessage: 1, + maxSharedFolderInviteLinks: 3, + maxSharedFolderJoin: 2 ) } @@ -47,7 +51,9 @@ public struct UserLimitsConfiguration: Equatable { maxUploadFileParts: Int32, maxAboutLength: Int32, maxAnimatedEmojisInText: Int32, - maxReactionsPerMessage: Int32 + maxReactionsPerMessage: Int32, + maxSharedFolderInviteLinks: Int32, + maxSharedFolderJoin: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -62,6 +68,8 @@ public struct UserLimitsConfiguration: Equatable { self.maxAboutLength = maxAboutLength self.maxAnimatedEmojisInText = maxAnimatedEmojisInText self.maxReactionsPerMessage = maxReactionsPerMessage + self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks + self.maxSharedFolderJoin = maxSharedFolderJoin } } @@ -99,5 +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("community_invites_limit", orElse: 3) + self.maxSharedFolderJoin = getValue("communities_joined_limit", orElse: 2) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 09f322077f..76897a712b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -49,6 +49,8 @@ public enum EngineConfiguration { public let maxAboutLength: Int32 public let maxAnimatedEmojisInText: Int32 public let maxReactionsPerMessage: Int32 + public let maxSharedFolderInviteLinks: Int32 + public let maxSharedFolderJoin: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -67,7 +69,9 @@ public enum EngineConfiguration { maxUploadFileParts: Int32, maxAboutLength: Int32, maxAnimatedEmojisInText: Int32, - maxReactionsPerMessage: Int32 + maxReactionsPerMessage: Int32, + maxSharedFolderInviteLinks: Int32, + maxSharedFolderJoin: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -82,6 +86,8 @@ public enum EngineConfiguration { self.maxAboutLength = maxAboutLength self.maxAnimatedEmojisInText = maxAnimatedEmojisInText self.maxReactionsPerMessage = maxReactionsPerMessage + self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks + self.maxSharedFolderJoin = maxSharedFolderJoin } } } @@ -131,7 +137,9 @@ public extension EngineConfiguration.UserLimits { maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts, maxAboutLength: userLimitsConfiguration.maxAboutLength, maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText, - maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage + maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage, + maxSharedFolderInviteLinks: userLimitsConfiguration.maxSharedFolderInviteLinks, + maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 50f3fd57b0..cf31dece8b 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(limit: Int32, premiumLimit: Int32) } public struct ExportedChatFolderLink: Equatable { @@ -47,8 +59,25 @@ 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 + |> `catch` { error -> Signal in + if error.errorDescription == "INVITES_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(ExportChatFolderError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks)) + } else { + return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks)) + } + } + } else { + return .fail(.generic) + } } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in @@ -178,9 +207,9 @@ public enum RevokeChatFolderLinkError { case generic } -func _internal_revokeChatFolderLink(account: Account, filterId: Int32, link: ExportedChatFolderLink) -> Signal { +func _internal_deleteChatFolderLink(account: Account, filterId: Int32, link: ExportedChatFolderLink) -> Signal { return account.network.request(Api.functions.communities.deleteExportedInvite(community: .inputCommunityDialogFilter(filterId: filterId), slug: link.slug)) - |> mapError { _ -> RevokeChatFolderLinkError in + |> mapError { error -> RevokeChatFolderLinkError in return .generic } |> ignoreValues @@ -248,9 +277,12 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal Signal Signal Signal { @@ -311,8 +361,40 @@ 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 + |> `catch` { error -> Signal in + if error.errorDescription == "DIALOG_FILTERS_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.dialogFilterLimitExceeded(limit: userPremiumLimits.maxFoldersCount, premiumLimit: userPremiumLimits.maxFoldersCount)) + } else { + return .fail(.dialogFilterLimitExceeded(limit: userDefaultLimits.maxFoldersCount, premiumLimit: userPremiumLimits.maxFoldersCount)) + } + } + } else if error.errorDescription == "FILTERS_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.sharedFolderLimitExceeded(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } else { + return .fail(.sharedFolderLimitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } + } + } else { + return .fail(.generic) + } } |> mapToSignal { result -> Signal in account.stateManager.addUpdates(result) @@ -321,3 +403,157 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi } } } + +public final class ChatFolderUpdates: Equatable { + fileprivate let folderId: Int32 + fileprivate let title: String + fileprivate let missingPeers: [Api.Peer] + fileprivate let chats: [Api.Chat] + fileprivate let users: [Api.User] + + public var availableChatsToJoin: Int { + return self.missingPeers.count + } + + public var chatFolderLinkContents: ChatFolderLinkContents { + var peers: [EnginePeer] = [] + for missingPeer in self.missingPeers { + for chat in chats { + if chat.peerId == missingPeer.peerId { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + peers.append(EnginePeer(peer)) + } + } + } + } + + return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: peers, alreadyMemberPeerIds: Set()) + } + + fileprivate init( + folderId: Int32, + title: String, + missingPeers: [Api.Peer], + chats: [Api.Chat], + users: [Api.User] + ) { + self.folderId = folderId + self.title = title + self.missingPeers = missingPeers + self.chats = chats + self.users = users + } + + public static func ==(lhs: ChatFolderUpdates, rhs: ChatFolderUpdates) -> Bool { + if lhs.folderId != rhs.folderId { + return false + } + if lhs.missingPeers.map(\.peerId) != rhs.missingPeers.map(\.peerId) { + return false + } + return true + } +} + +func _internal_getChatFolderUpdates(account: Account, folderId: Int32) -> Signal { + return account.network.request(Api.functions.communities.getCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .single(nil) + } + switch result { + case let .communityUpdates(missingPeers, chats, users): + return account.postbox.transaction { transaction -> ChatFolderUpdates? in + for filter in _internal_currentChatListFilters(transaction: transaction) { + if case let .filter(id, title, _, _) = filter, id == folderId { + return ChatFolderUpdates(folderId: folderId, title: title, missingPeers: missingPeers, chats: chats, users: users) + } + } + return nil + } + } + } +} + +func _internal_joinAvailableChatsInFolder(account: Account, updates: ChatFolderUpdates, peerIds: [EnginePeer.Id]) -> Signal { + return account.postbox.transaction { transaction -> [Api.InputPeer] in + return peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { inputPeers -> Signal in + return account.network.request(Api.functions.communities.joinCommunityUpdates(community: .inputCommunityDialogFilter(filterId: updates.folderId), peers: inputPeers)) + |> `catch` { error -> Signal in + if error.errorDescription == "DIALOG_FILTERS_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.dialogFilterLimitExceeded(limit: userPremiumLimits.maxFoldersCount, premiumLimit: userPremiumLimits.maxFoldersCount)) + } else { + return .fail(.dialogFilterLimitExceeded(limit: userDefaultLimits.maxFoldersCount, premiumLimit: userPremiumLimits.maxFoldersCount)) + } + } + } else if error.errorDescription == "FILTERS_TOO_MUCH" { + return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in + return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) + } + |> castError(JoinChatFolderLinkError.self) + |> mapToSignal { appConfiguration, isPremium -> Signal in + let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) + let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) + + if isPremium { + return .fail(.sharedFolderLimitExceeded(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } else { + return .fail(.sharedFolderLimitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } + } + } else { + return .fail(.generic) + } + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return .complete() + } + } +} + +func _internal_hideChatFolderUpdates(account: Account, folderId: Int32) -> Signal { + return account.network.request(Api.functions.communities.hideCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId))) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues +} + +func _internal_leaveChatFolder(account: Account, folderId: Int32, removePeerIds: [EnginePeer.Id]) -> Signal { + return account.postbox.transaction { transaction -> [Api.InputPeer] in + return removePeerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer) + } + |> mapToSignal { inputPeers -> Signal in + return account.network.request(Api.functions.communities.leaveCommunity(community: .inputCommunityDialogFilter(filterId: folderId), peers: inputPeers)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates = updates { + account.stateManager.addUpdates(updates) + } + return account.postbox.transaction { transaction -> Void in + } + |> ignoreValues + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift index f126fde254..24e962a7e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ConvertGroupToSupergroup.swift @@ -9,7 +9,7 @@ public enum ConvertGroupToSupergroupError { case tooManyChannels } -func _internal_convertGroupToSupergroup(account: Account, peerId: PeerId) -> Signal { +func _internal_convertGroupToSupergroup(account: Account, peerId: PeerId, additionalProcessing: ((EnginePeer.Id) -> Signal)?) -> Signal { return account.network.request(Api.functions.messages.migrateChat(chatId: peerId.id._internalGetInt64Value())) |> mapError { error -> ConvertGroupToSupergroupError in if error.errorDescription == "CHANNELS_TOO_MUCH" { @@ -20,7 +20,6 @@ func _internal_convertGroupToSupergroup(account: Account, peerId: PeerId) -> Sig } |> timeout(5.0, queue: Queue.concurrentDefaultQueue(), alternate: .fail(.generic)) |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) var createdPeerId: PeerId? for message in updates.messages { if apiMessagePeerId(message) != peerId { @@ -30,19 +29,35 @@ func _internal_convertGroupToSupergroup(account: Account, peerId: PeerId) -> Sig } if let createdPeerId = createdPeerId { - return account.postbox.multiplePeersView([createdPeerId]) - |> filter { view in - return view.peers[createdPeerId] != nil + let additionalProcessingValue: Signal = additionalProcessing?(createdPeerId) ?? Signal.complete() + + return additionalProcessingValue + |> map { _ -> Bool in } + |> castError(ConvertGroupToSupergroupError.self) + |> then(Signal.single(true)) + |> mapToSignal { _ ->Signal in + account.stateManager.addUpdates(updates) + + return _internal_fetchAndUpdateCachedPeerData(accountPeerId: account.peerId, peerId: createdPeerId, network: account.network, postbox: account.postbox) + |> castError(ConvertGroupToSupergroupError.self) + |> mapToSignal { _ -> Signal in + return account.postbox.multiplePeersView([createdPeerId]) + |> filter { view in + return view.peers[createdPeerId] != nil + } + |> take(1) + |> map { _ in + return createdPeerId + } + |> mapError { _ -> ConvertGroupToSupergroupError in + } + |> timeout(5.0, queue: Queue.concurrentDefaultQueue(), alternate: .fail(.generic)) + } } - |> take(1) - |> map { _ in - return createdPeerId - } - |> mapError { _ -> ConvertGroupToSupergroupError in - } - |> timeout(5.0, queue: Queue.concurrentDefaultQueue(), alternate: .fail(.generic)) + } else { + account.stateManager.addUpdates(updates) + return .fail(.generic) } - return .fail(.generic) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index acd8ddac9a..51ce66deb8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -134,8 +134,8 @@ public extension TelegramEngine { return _internal_chatOnlineMembers(postbox: self.account.postbox, network: self.account.network, peerId: peerId) } - public func convertGroupToSupergroup(peerId: PeerId) -> Signal { - return _internal_convertGroupToSupergroup(account: self.account, peerId: peerId) + public func convertGroupToSupergroup(peerId: PeerId, additionalProcessing: ((EnginePeer.Id) -> Signal)? = nil) -> Signal { + return _internal_convertGroupToSupergroup(account: self.account, peerId: peerId, additionalProcessing: additionalProcessing) } public func createGroup(title: String, peerIds: [PeerId], ttlPeriod: Int32?) -> Signal { @@ -1046,8 +1046,8 @@ public extension TelegramEngine { return _internal_editChatFolderLink(account: self.account, filterId: filterId, link: link, title: title, peerIds: peerIds, revoke: revoke) } - public func revokeChatFolderLink(filterId: Int32, link: ExportedChatFolderLink) -> Signal { - return _internal_revokeChatFolderLink(account: self.account, filterId: filterId, link: link) + public func deleteChatFolderLink(filterId: Int32, link: ExportedChatFolderLink) -> Signal { + return _internal_deleteChatFolderLink(account: self.account, filterId: filterId, link: link) } public func checkChatFolderLink(slug: String) -> Signal { @@ -1057,6 +1057,22 @@ public extension TelegramEngine { public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal { return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds) } + + public func getChatFolderUpdates(folderId: Int32) -> Signal { + return _internal_getChatFolderUpdates(account: self.account, folderId: folderId) + } + + public func joinAvailableChatsInFolder(updates: ChatFolderUpdates, peerIds: [EnginePeer.Id]) -> Signal { + return _internal_joinAvailableChatsInFolder(account: self.account, updates: updates, peerIds: peerIds) + } + + public func hideChatFolderUpdates(folderId: Int32) -> Signal { + return _internal_hideChatFolderUpdates(account: self.account, folderId: folderId) + } + + public func leaveChatFolder(folderId: Int32, removePeerIds: [EnginePeer.Id]) -> Signal { + return _internal_leaveChatFolder(account: self.account, folderId: folderId, removePeerIds: removePeerIds) + } } } 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..5ffd7a2460 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -15,29 +15,33 @@ import SolidRoundedButtonComponent import PresentationDataUtils import Markdown import UndoUI +import PremiumUI private final class ChatFolderLinkPreviewScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let slug: String + let subject: ChatFolderLinkPreviewScreen.Subject let linkContents: ChatFolderLinkContents? + let completion: (() -> Void)? init( context: AccountContext, - slug: String, - linkContents: ChatFolderLinkContents? + subject: ChatFolderLinkPreviewScreen.Subject, + linkContents: ChatFolderLinkContents?, + completion: (() -> Void)? ) { self.context = context - self.slug = slug + self.subject = subject self.linkContents = linkContents + self.completion = completion } static func ==(lhs: ChatFolderLinkPreviewScreenComponent, rhs: ChatFolderLinkPreviewScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } - if lhs.slug != rhs.slug { + if lhs.subject != rhs.subject { return false } if lhs.linkContents !== rhs.linkContents { @@ -66,6 +70,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 +91,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] = [:] @@ -100,6 +110,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { private var joinDisposable: Disposable? + private var inProgress: Bool = false + override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -240,7 +252,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 +270,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 +305,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 @@ -304,10 +327,19 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } let titleString: String + var allChatsAdded = false if let linkContents = component.linkContents { //TODO:localize - if linkContents.localFilterId != nil { - if self.selectedItems.count == 1 { + if case .remove = component.subject { + titleString = "Remove Folder" + } else if linkContents.localFilterId != nil { + if linkContents.alreadyMemberPeerIds == Set(linkContents.peers.map(\.id)) { + allChatsAdded = true + } + + if allChatsAdded { + titleString = "Add Folder" + } else if self.selectedItems.count == 1 { titleString = "Add \(self.selectedItems.count) chat" } else { titleString = "Add \(self.selectedItems.count) chats" @@ -331,19 +363,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 !allChatsAdded, 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 +390,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 } @@ -362,7 +399,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let text: String if let linkContents = component.linkContents { - if linkContents.localFilterId == nil { + if case .remove = component.subject { + text = "Do you want to quit the chats you joined when\nadding the folder \(linkContents.title ?? "Folder")?" + } else if allChatsAdded { + text = "You have already added this\nfolder and its chats." + } else if linkContents.localFilterId == nil { text = "Do you want to add a new chat folder\nand join its groups and channels?" } else { let chatCountString: String @@ -404,7 +445,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 +487,34 @@ 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 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 { - 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: {}, @@ -487,13 +547,39 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } let listHeaderTitle: String - if self.selectedItems.count == 1 { - listHeaderTitle = "1 CHAT IN FOLDER TO JOIN" + if let linkContents = component.linkContents { + if case .remove = component.subject { + if linkContents.peers.count == 1 { + listHeaderTitle = "1 CHAT TO QUIT" + } else { + listHeaderTitle = "\(linkContents.peers.count) CHATS TO QUIT" + } + } else if allChatsAdded { + if linkContents.peers.count == 1 { + listHeaderTitle = "1 CHAT IN THIS FOLDER" + } else { + listHeaderTitle = "\(linkContents.peers.count) CHATS IN THIS FOLDER" + } + } else { + if linkContents.peers.count == 1 { + listHeaderTitle = "1 CHAT IN FOLDER TO JOIN" + } else { + listHeaderTitle = "\(linkContents.peers.count) CHATS IN FOLDER TO JOIN" + } + } } else { - listHeaderTitle = "\(self.selectedItems.count) CHATS IN FOLDER TO JOIN" + listHeaderTitle = " " + } + + 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 +603,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 || allChatsAdded + } + 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)) @@ -534,9 +666,21 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { initialContentHeight += 24.0 let actionButtonTitle: String - if let linkContents = component.linkContents { + if case .remove = component.subject { + if self.selectedItems.isEmpty { + actionButtonTitle = "Remove Folder" + } else { + actionButtonTitle = "Remove Folder and Chats" + } + } else if allChatsAdded { + actionButtonTitle = "OK" + } else 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" } @@ -548,57 +692,81 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { transition: transition, component: AnyComponent(SolidRoundedButtonComponent( title: actionButtonTitle, - badge: (self.selectedItems.isEmpty) ? nil : "\(self.selectedItems.count)", + 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, + isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, animationName: nil, iconPosition: .right, iconSpacing: 4.0, - isLoading: component.linkContents == nil, + isLoading: self.inProgress, 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 case let .remove(folderId) = component.subject { + self.inProgress = true + self.state?.updated(transition: .immediate) + + component.completion?() + + 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 { - self.joinDisposable = (component.context.engine.peers.joinChatFolderLink(slug: component.slug, peerIds: Array(self.selectedItems)) - |> deliverOnMainQueue).start(completed: { [weak self] in + let joinSignal: Signal + switch component.subject { + case .remove: + return + case let .slug(slug): + joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems)) + case let .updates(updates): + joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems)) + } + + self.inProgress = true + self.state?.updated(transition: .immediate) + + self.joinDisposable = (joinSignal + |> 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 let .dialogFilterLimitExceeded(limit, _): + let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: limit, action: {}) + controller.push(limitController) + controller.dismiss() + case let .sharedFolderLimitExceeded(limit, _): + let limitController = PremiumLimitScreen(context: component.context, subject: .membershipInSharedFolders, count: limit, 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() } } - - /*if self.selectedItems.isEmpty { - controller.dismiss() - } else if let link = component.link { - let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) } - - let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() - let text: String - if selectedPeers.count == 1 { - text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else if selectedPeers.count == 2 { - text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else { - text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) - - controller.dismiss() - } else { - controller.dismiss() - }*/ } )), environment: {}, @@ -613,6 +781,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 @@ -660,30 +841,26 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case slug(String) + case updates(ChatFolderUpdates) + case remove(folderId: Int32) + } + private let context: AccountContext - private var linkContents: ChatFolderLinkContents? private var linkContentsDisposable: Disposable? private var isDismissed: Bool = false - public init(context: AccountContext, slug: String) { + public init(context: AccountContext, subject: Subject, contents: ChatFolderLinkContents, completion: (() -> Void)? = nil) { self.context = context - super.init(context: context, component: ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: nil), navigationBarAppearance: .none) + super.init(context: context, component: ChatFolderLinkPreviewScreenComponent(context: context, subject: subject, linkContents: contents, completion: completion), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true - - self.linkContentsDisposable = (context.engine.peers.checkChatFolderLink(slug: slug) - //|> 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.automaticallyControlPresentationContextLayout = false } required public init(coder aDecoder: NSCoder) { @@ -694,6 +871,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) diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index f317960b4d..9b4dc89e06 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -265,6 +265,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openChatFolderUpdates: { }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 54bbc46a83..9cd461f90b 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -753,7 +753,48 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } case let .chatFolder(slug): if let navigationController = navigationController { - navigationController.pushViewController(ChatFolderLinkPreviewScreen(context: context, slug: slug)) + let signal = context.engine.peers.checkChatFolderLink(slug: slug) + + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + present(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.35, queue: Queue.mainQueue()) + + let disposable = MetaDisposable() + let progressDisposable = progressSignal.start() + cancelImpl = { + disposable.set(nil) + } + disposable.set((signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).start(next: { [weak navigationController] result in + guard let navigationController else { + return + } + navigationController.pushViewController(ChatFolderLinkPreviewScreen(context: context, subject: .slug(slug), contents: result)) + }, error: { error in + let errorText: String + switch error { + case .generic: + errorText = "The folder link has expired." + } + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + })) + dismissInput() } } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index c388cc39cd..95927fdbaa 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -837,7 +837,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } handleResolvedUrl(.premiumOffer(reference: reference)) - } else if parsedUrl.host == "folder" { + } else if parsedUrl.host == "list" { if let components = URLComponents(string: "/?" + query) { var slug: String? if let queryItems = components.queryItems { @@ -850,7 +850,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } if let slug = slug { - convertedUrl = "https://t.me/folder/\(slug)" + convertedUrl = "https://t.me/list/\(slug)" } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index a090fe2b0c..03c4344fec 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -350,40 +350,42 @@ enum PeerInfoMembersData: Equatable { } private func peerInfoScreenInputData(context: AccountContext, peerId: EnginePeer.Id, isSettings: Bool) -> Signal { - return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> map { peer -> PeerInfoScreenInputData in - guard let peer = peer else { - return .none - } - if case let .user(user) = peer { - if isSettings && user.id == context.account.peerId { - return .settings - } else { - let kind: PeerInfoScreenInputUserKind - if user.flags.contains(.isSupport) { - kind = .support - } else if user.botInfo != nil { - kind = .bot + return `deferred` { + return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + guard let peer = peer else { + return .single(.none) + } + if case let .user(user) = peer { + if isSettings && user.id == context.account.peerId { + return .single(.settings) } else { - kind = .user + let kind: PeerInfoScreenInputUserKind + if user.flags.contains(.isSupport) { + kind = .support + } else if user.botInfo != nil { + kind = .bot + } else { + kind = .user + } + return .single(.user(userId: user.id, secretChatId: nil, kind: kind)) } - return .user(userId: user.id, secretChatId: nil, kind: kind) - } - } else if case let .channel(channel) = peer { - if case .group = channel.info { - return .group(groupId: channel.id) + } else if case let .channel(channel) = peer { + if case .group = channel.info { + return .single(.group(groupId: channel.id)) + } else { + return .single(.channel) + } + } else if case let .legacyGroup(group) = peer { + return .single(.group(groupId: group.id)) + } else if case let .secretChat(secretChat) = peer { + return .single(.user(userId: secretChat.regularPeerId, secretChatId: peer.id, kind: .user)) } else { - return .channel + return .single(.none) } - } else if case let .legacyGroup(group) = peer { - return .group(groupId: group.id) - } else if case let .secretChat(secretChat) = peer { - return .user(userId: secretChat.regularPeerId, secretChatId: peer.id, kind: .user) - } else { - return .none } + |> distinctUntilChanged } - |> distinctUntilChanged } func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal { @@ -547,6 +549,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, hintGroupInCommon: PeerId?, existingRequestsContext: PeerInvitationImportersContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic) -> Signal { return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: isSettings) |> mapToSignal { inputData -> Signal in + let wasUpgradedGroup = Atomic(value: nil) + switch inputData { case .none, .settings: return .single(PeerInfoScreenData( @@ -914,7 +918,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen threadData, context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) ) - |> map { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView -> PeerInfoScreenData in + |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, threadData, preferencesView -> Signal in var discussionPeer: Peer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { discussionPeer = peer @@ -931,6 +935,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen var canManageInvitations = false if let group = peerViewMainPeer(peerView) as? TelegramGroup { + let previousValue = wasUpgradedGroup.swap(group.migrationReference != nil) + if group.migrationReference != nil, let previousValue, !previousValue { + return .never() + } + if case .creator = group.role { canManageInvitations = true } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) { @@ -960,7 +969,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue - return PeerInfoScreenData( + return .single(PeerInfoScreenData( peer: peerView.peers[groupId], chatPeer: peerView.peers[groupId], cachedData: peerView.cachedData, @@ -981,7 +990,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen threadData: threadData, appConfiguration: appConfiguration, isPowerSavingEnabled: nil - ) + )) } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f7b0619e0d..abc2b0868a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1847,6 +1847,8 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemAdmins = 105 let ItemMemberRequests = 106 let ItemReactions = 107 + let ItemTopics = 108 + let ItemTopicsText = 109 var canViewAdminsAndBanned = false @@ -1876,6 +1878,33 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenPreHistorySetup() })) + var canSetupTopics = false + if case .creator = group.role { + canSetupTopics = true + } + var topicsLimitedReason: TopicsLimitedReason? + if let appConfiguration = data.appConfiguration { + var minParticipants = 200 + if let data = appConfiguration.data, let value = data["forum_upgrade_participants_min"] as? Double { + minParticipants = Int(value) + } + if Int(group.participantCount) < minParticipants { + topicsLimitedReason = .participants(minParticipants) + } + } + + if canSetupTopics { + items[.peerPublicSettings]!.append(PeerInfoScreenSwitchItem(id: ItemTopics, text: presentationData.strings.PeerInfo_OptionTopics, value: false, icon: UIImage(bundleImageName: "Settings/Menu/Topics"), isLocked: topicsLimitedReason != nil, toggled: { value in + if let topicsLimitedReason = topicsLimitedReason { + interaction.displayTopicsLimited(topicsLimitedReason) + } else { + interaction.toggleForumTopics(value) + } + })) + + items[.peerPublicSettings]!.append(PeerInfoScreenCommentItem(id: ItemTopicsText, text: presentationData.strings.PeerInfo_OptionTopicsText)) + } + let label: String if let cachedData = data.cachedData as? CachedGroupData, case let .known(allowedReactions) = cachedData.allowedReactions { switch allowedReactions { @@ -2252,7 +2281,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.setChannelForumMode(id: strongSelf.peerId, isForum: value).start() + strongSelf.toggleForumTopics(isEnabled: value) }, displayTopicsLimited: { [weak self] reason in guard let self else { @@ -6605,6 +6634,61 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.controller?.push(peerAllowedReactionListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func toggleForumTopics(isEnabled: Bool) { + guard let data = self.data, let peer = data.peer else { + return + } + if peer is TelegramGroup { + if isEnabled { + let context = self.context + let signal: Signal = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId, additionalProcessing: { upgradedPeerId -> Signal in + return context.engine.peers.setChannelForumMode(id: upgradedPeerId, isForum: isEnabled) + }) + |> map(Optional.init) + |> `catch` { [weak self] error -> Signal in + switch error { + case .tooManyChannels: + Queue.mainQueue().async { + self?.controller?.push(oldChannelsController(context: context, intent: .upgrade)) + } + default: + break + } + return .single(nil) + } + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .single(nil) + } + return .single(upgradedPeerId) + } + |> deliverOnMainQueue + + let _ = signal.start(next: { [weak self] resultPeerId in + guard let self else { + return + } + guard let resultPeerId else { + return + } + + let _ = (self.context.engine.peers.setChannelForumMode(id: resultPeerId, isForum: isEnabled) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let controller = self.controller else { + return + } + /*if let navigationController = controller.navigationController as? NavigationController { + rebuildControllerStackAfterSupergroupUpgrade(controller: controller, navigationController: navigationController) + }*/ + controller.dismiss() + }) + }) + } + } else { + let _ = self.context.engine.peers.setChannelForumMode(id: self.peerId, isForum: isEnabled).start() + } + } + private func editingToggleMessageSignatures(value: Bool) { self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).start()) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 344e09cc89..aafff67bd3 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1676,6 +1676,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .files case .accounts: mappedSubject = .accounts + case .linksPerSharedFolder: + mappedSubject = .linksPerSharedFolder + case .membershipInSharedFolders: + mappedSubject = .membershipInSharedFolders } return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, action: action) } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 81bab5f2ab..aaa4abbdaa 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -1180,17 +1180,18 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var contentHeight: CGFloat = 20.0 let margin: CGFloat = 12.0 + let leftMargin = 12.0 + layout.insets(options: []).left let buttonTextSize = self.undoButtonTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let buttonMinX: CGFloat if self.undoButtonNode.supernode != nil { - buttonMinX = layout.size.width - layout.safeInsets.left - rightInset - buttonTextSize.width - margin * 2.0 + buttonMinX = layout.size.width - layout.safeInsets.left - rightInset - buttonTextSize.width - leftMargin * 2.0 } else { buttonMinX = layout.size.width - layout.safeInsets.left - rightInset } - let titleSize = self.titleNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - margin, height: .greatestFiniteMagnitude)) - let textSize = self.textNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - margin, height: .greatestFiniteMagnitude)) + let titleSize = self.titleNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude)) + let textSize = self.textNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude)) if !titleSize.width.isZero { contentHeight += titleSize.height + 1.0 @@ -1209,8 +1210,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } } - var panelFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) - var panelWrapperFrame = CGRect(origin: CGPoint(x: margin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) + var panelFrame = CGRect(origin: CGPoint(x: leftMargin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - leftMargin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) + var panelWrapperFrame = CGRect(origin: CGPoint(x: leftMargin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - leftMargin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) if case .top = self.placementPosition { panelFrame.origin.y = insets.top + margin @@ -1219,12 +1220,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { transition.updateFrame(node: self.panelNode, frame: panelFrame) transition.updateFrame(node: self.panelWrapperNode, frame: panelWrapperFrame) - self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - margin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight) + self.effectView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width - leftMargin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight) - let buttonTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - margin * 2.0, y: floor((contentHeight - buttonTextSize.height) / 2.0)), size: buttonTextSize) + let buttonTextFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - leftMargin * 2.0, y: floor((contentHeight - buttonTextSize.height) / 2.0)), size: buttonTextSize) transition.updateFrame(node: self.undoButtonTextNode, frame: buttonTextFrame) - let undoButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - margin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + margin, height: contentHeight)) + let undoButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - rightInset - buttonTextSize.width - 8.0 - leftMargin * 2.0, y: 0.0), size: CGSize(width: layout.safeInsets.right + rightInset + buttonTextSize.width + 8.0 + leftMargin, height: contentHeight)) self.undoButtonNode.frame = undoButtonFrame self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: undoButtonFrame.minX, height: contentHeight)) diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 099656200b..758899cecf 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -418,7 +418,7 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { return .theme(pathComponents[1]) - } else if pathComponents[0] == "folder" { + } else if pathComponents[0] == "list" || pathComponents[0] == "folder" { return .chatFolder(slug: pathComponents[1]) } else if pathComponents.count == 3 && pathComponents[0] == "c" { if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) { diff --git a/versions.json b/versions.json index 78c0df0670..df40ca8ac5 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "9.5.3", + "app": "9.5.4", "bazel": "5.3.1", "xcode": "14.2" }