diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f5bef0abcc..0484642b5d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -298,6 +298,7 @@ public enum ResolvedUrl { case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?) case invoice(slug: String, invoice: TelegramMediaInvoice?) case premiumOffer(reference: String?) + case chatFolder(slug: String) } public enum NavigateToChatKeepStack { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 7552d56592..45d5181b52 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -88,6 +88,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatListTitleView", "//submodules/AvatarNode:AvatarNode", "//submodules/AvatarVideoNode:AvatarVideoNode", + "//submodules/InviteLinksUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index fa9cb9ab72..aaa54fd9cc 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -43,6 +43,7 @@ import PeerInfoUI import ComponentDisplayAdapters import ChatListHeaderComponent import ChatListTitleView +import InviteLinksUI private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -1539,6 +1540,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) } + + //TODO:localize + + for filter in filters { + if filter.id == filterId, case let .filter(_, _, _, 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) + }, action: { c, f in + c.dismiss(completion: { + guard let strongSelf = self else { + return + } + strongSelf.shareFolder(filterId: filterId, data: data) + }) + }))) + } + + break + } + } + break } } @@ -2676,6 +2699,11 @@ 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 askForFilterRemoval(id: Int32) { let actionSheet = ActionSheetController(presentationData: self.presentationData) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index e33cadd481..d78e95e6a1 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -14,6 +14,7 @@ import ItemListPeerActionItem import AvatarNode import ChatListFilterSettingsHeaderItem import PremiumUI +import InviteLinksUI private enum FilterSection: Int32, Hashable { case include @@ -32,6 +33,8 @@ private final class ChatListFilterPresetControllerArguments { let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void let focusOnName: () -> Void let expandSection: (FilterSection) -> Void + let createLink: () -> Void + let openLink: (ExportedChatFolderLink) -> Void init( context: AccountContext, @@ -44,7 +47,9 @@ private final class ChatListFilterPresetControllerArguments { deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void, deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void, focusOnName: @escaping () -> Void, - expandSection: @escaping (FilterSection) -> Void + expandSection: @escaping (FilterSection) -> Void, + createLink: @escaping () -> Void, + openLink: @escaping (ExportedChatFolderLink) -> Void ) { self.context = context self.updateState = updateState @@ -57,6 +62,8 @@ private final class ChatListFilterPresetControllerArguments { self.deleteExcludeCategory = deleteExcludeCategory self.focusOnName = focusOnName self.expandSection = expandSection + self.createLink = createLink + self.openLink = openLink } } @@ -65,6 +72,7 @@ private enum ChatListFilterPresetControllerSection: Int32 { case name case includePeers case excludePeers + case inviteLinks } private enum ChatListFilterPresetEntryStableId: Hashable { @@ -76,6 +84,7 @@ private enum ChatListFilterPresetEntryStableId: Hashable { case excludeCategory(ChatListFilterExcludeCategory) case includeExpand case excludeExpand + case inviteLink(String) } private enum ChatListFilterPresetEntrySortId: Comparable { @@ -83,6 +92,9 @@ private enum ChatListFilterPresetEntrySortId: Comparable { case topIndex(Int) case includeIndex(Int) case excludeIndex(Int) + case bottomIndex(Int) + case inviteLink(Int) + case inviteLinkFooter static func <(lhs: ChatListFilterPresetEntrySortId, rhs: ChatListFilterPresetEntrySortId) -> Bool { switch lhs { @@ -103,6 +115,12 @@ private enum ChatListFilterPresetEntrySortId: Comparable { return true case .excludeIndex: return true + case .bottomIndex: + return true + case .inviteLink: + return true + case .inviteLinkFooter: + return true } case let .includeIndex(lhsIndex): switch rhs { @@ -114,6 +132,12 @@ private enum ChatListFilterPresetEntrySortId: Comparable { return lhsIndex < rhsIndex case .excludeIndex: return true + case .bottomIndex: + return true + case .inviteLink: + return true + case .inviteLinkFooter: + return true } case let .excludeIndex(lhsIndex): switch rhs { @@ -125,6 +149,63 @@ private enum ChatListFilterPresetEntrySortId: Comparable { return false case let .excludeIndex(rhsIndex): return lhsIndex < rhsIndex + case .bottomIndex: + return true + case .inviteLink: + return true + case .inviteLinkFooter: + return true + } + case let .bottomIndex(lhsIndex): + switch rhs { + case .screenHeader: + return false + case .topIndex: + return false + case .includeIndex: + return false + case .excludeIndex: + return false + case let .bottomIndex(rhsIndex): + return lhsIndex < rhsIndex + case .inviteLink: + return true + case .inviteLinkFooter: + return true + } + case let .inviteLink(lhsIndex): + switch rhs { + case .screenHeader: + return false + case .topIndex: + return false + case .includeIndex: + return false + case .excludeIndex: + return false + case .bottomIndex: + return false + case let .inviteLink(rhsIndex): + return lhsIndex < rhsIndex + case .inviteLinkFooter: + return true + } + case .inviteLinkFooter: + switch rhs { + case .screenHeader: + return false + case .topIndex: + return false + case .includeIndex: + return false + case .excludeIndex: + return false + case .bottomIndex: + return false + case .inviteLink: + return false + case .inviteLinkFooter: + return false } } } @@ -235,6 +316,10 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case excludePeerInfo(String) case includeExpand(String) case excludeExpand(String) + case inviteLinkHeader + case inviteLinkCreate + case inviteLink(Int, ExportedChatFolderLink) + case inviteLinkInfo var section: ItemListSectionId { switch self { @@ -246,6 +331,8 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ChatListFilterPresetControllerSection.includePeers.rawValue case .excludePeersHeader, .addExcludePeer, .excludeCategory, .excludePeer, .excludePeerInfo, .excludeExpand: return ChatListFilterPresetControllerSection.excludePeers.rawValue + case .inviteLinkHeader, .inviteLinkCreate, .inviteLink, .inviteLinkInfo: + return ChatListFilterPresetControllerSection.inviteLinks.rawValue } } @@ -281,6 +368,14 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return .peer(peer.peerId) case let .excludePeer(_, peer, _): return .peer(peer.peerId) + case .inviteLinkHeader: + return .index(11) + case .inviteLinkCreate: + return .index(12) + case let .inviteLink(_, link): + return .inviteLink(link.link) + case .inviteLinkInfo: + return .index(13) } } @@ -316,6 +411,14 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return .excludeIndex(999) case .excludePeerInfo: return .excludeIndex(1000) + case .inviteLinkHeader: + return .bottomIndex(0) + case .inviteLinkCreate: + return .bottomIndex(1) + case let .inviteLink(index, _): + return .inviteLink(index) + case .inviteLinkInfo: + return .inviteLinkFooter } } @@ -413,6 +516,23 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: { arguments.expandSection(.exclude) }) + case .inviteLinkHeader: + //TODO:localize + return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", sectionId: self.section) + case .inviteLinkCreate: + //TODO:localize + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "Share Folder with Others", sectionId: self.section, editing: false, action: { + arguments.createLink() + }) + case let .inviteLink(_, link): + return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks) { invite in + arguments.openLink(invite) + } contextAction: { invite, node, gesture in + //arguments.linkContextAction(invite, canEdit, node, gesture) + } + 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) } } } @@ -455,7 +575,7 @@ private struct ChatListFilterPresetControllerState: Equatable { } } -private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32) -> [ChatListFilterPresetEntry] { +private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?) -> [ChatListFilterPresetEntry] { var entries: [ChatListFilterPresetEntry] = [] if isNewFilter { @@ -531,6 +651,19 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) + if !isNewFilter, let inviteLinks { + entries.append(.inviteLinkHeader) + entries.append(.inviteLinkCreate) + + var index = 0 + for link in inviteLinks { + entries.append(.inviteLink(index, link)) + index += 1 + } + + entries.append(.inviteLinkInfo) + } + return entries } @@ -887,7 +1020,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let presentationData = context.sharedContext.currentPresentationData.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) if let data = filter.data { switch chatListFilterType(data) { case .generic: @@ -924,6 +1057,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var dismissImpl: (() -> Void)? var focusOnNameImpl: (() -> Void)? + 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))) + } + let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:]) let stateWithPeers = statePromise.get() |> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in @@ -1025,7 +1164,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let state = stateValue.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in @@ -1047,7 +1186,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let state = stateValue.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in @@ -1124,6 +1263,47 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat state.expandedSections.insert(section) return state } + }, + 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)) + } + }) + })) + } + }, 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 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)) + } + } + }) + } + })) + } } ) @@ -1138,7 +1318,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat if currentPreset == nil { filterId = context.engine.peers.generateNewChatListFilterId(filters: filters) } - var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(isShared: currentPreset?.data?.isShared ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) var filters = filters if let _ = currentPreset { @@ -1182,10 +1362,11 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat context.account.postbox.peerView(id: context.account.peerId), context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) - ) + ), + sharedLinks.get() ) |> deliverOnMainQueue - |> map { presentationData, stateWithPeers, peerView, premiumLimits -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks -> (ItemListControllerState, (ItemListNodeState, Any)) in let (state, includePeers, excludePeers) = stateWithPeers let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false @@ -1206,7 +1387,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), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation) + 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) skipStateAnimation = false return (controllerState, (listState, arguments)) @@ -1261,7 +1442,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(isShared: currentPreset.data?.isShared ?? false, categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) if currentPresetWithoutPinnedPeers != filter { displaySaveAlert() return false diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 858bc153ba..b1f36d3720 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -56,8 +56,8 @@ private enum ChatListFilterPresetListEntryStableId: Hashable { case suggestedPreset(ChatListFilterData) case suggestedAddCustom case listHeader - case preset(Int32) case addItem + case preset(Int32) case listFooter } @@ -96,10 +96,10 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return 0 case .listHeader: return 100 - case let .preset(index, _, _, _, _, _, _, _, _): - return 101 + index.value case .addItem: - return 1000 + return 101 + case let .preset(index, _, _, _, _, _, _, _, _): + return 102 + index.value case .listFooter: return 1001 case .suggestedListHeader: @@ -219,9 +219,13 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present return true } + + entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection)) + + //TODO:localize + entries.append(.addItem(text: "Create a Folder", isEditing: state.isEditing)) + if !filters.isEmpty || suggestedFilters.isEmpty { - entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection)) - var folderCount = 0 for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { if case .allChats = filter { @@ -233,8 +237,6 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present } } - entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing)) - entries.append(.listFooter(presentationData.strings.ChatListFolderSettings_EditFoldersInfo)) } diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 029afd556d..2776218960 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -6,16 +6,17 @@ import LegacyComponents import TelegramPresentationData public struct CheckNodeTheme { - public let backgroundColor: UIColor - public let strokeColor: UIColor - public let borderColor: UIColor - public let overlayBorder: Bool - public let hasInset: Bool - public let hasShadow: Bool - public let filledBorder: Bool - public let borderWidth: CGFloat? + public var backgroundColor: UIColor + public var strokeColor: UIColor + public var borderColor: UIColor + public var overlayBorder: Bool + public var hasInset: Bool + public var hasShadow: Bool + public var filledBorder: Bool + public var borderWidth: CGFloat? + public var isDottedBorder: Bool - public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) { + public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil, isDottedBorder: Bool = false) { self.backgroundColor = backgroundColor self.strokeColor = strokeColor self.borderColor = borderColor @@ -24,6 +25,7 @@ public struct CheckNodeTheme { self.hasShadow = hasShadow self.filledBorder = filledBorder self.borderWidth = borderWidth + self.isDottedBorder = isDottedBorder } } @@ -168,6 +170,9 @@ public class CheckNode: ASDisplayNode { let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35) context.setStrokeColor(parameters.theme.borderColor.cgColor) + if parameters.theme.isDottedBorder { + context.setLineDash(phase: 0.0, lengths: [4.0, 4.0]) + } context.setLineWidth(borderWidth) let maybeScaleOut = { diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift new file mode 100644 index 0000000000..cfbea0bd96 --- /dev/null +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -0,0 +1,607 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import OverlayStatusController +import AccountContext +import AlertUI +import PresentationDataUtils +import AppBundle +import ContextUI +import TelegramStringFormatting +import ItemListPeerActionItem +import ItemListPeerItem +import ShareController +import UndoUI +import QrCodeUI + +private final class FolderInviteLinkListControllerArguments { + let context: AccountContext + let shareMainLink: (String) -> Void + let openMainLink: (String) -> Void + let copyLink: (String) -> Void + let mainLinkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void + let peerAction: (EnginePeer, Bool) -> Void + let generateLink: () -> Void + + init( + context: AccountContext, + shareMainLink: @escaping (String) -> Void, + openMainLink: @escaping (String) -> Void, + copyLink: @escaping (String) -> Void, + mainLinkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void, + peerAction: @escaping (EnginePeer, Bool) -> Void, + generateLink: @escaping () -> Void + ) { + self.context = context + self.shareMainLink = shareMainLink + self.openMainLink = openMainLink + self.copyLink = copyLink + self.mainLinkContextAction = mainLinkContextAction + self.peerAction = peerAction + self.generateLink = generateLink + } +} + +private enum InviteLinksListSection: Int32 { + case header + case mainLink + case peers +} + +private enum InviteLinksListEntry: ItemListNodeEntry { + enum StableId: Hashable { + case index(Int) + case peer(EnginePeer.Id) + } + + case header(String) + + case mainLinkHeader(String) + case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool) + + case peersHeader(String) + case peer(index: Int, peer: EnginePeer, isSelected: Bool, isEnabled: Bool) + case peersInfo(String) + + var section: ItemListSectionId { + switch self { + case .header: + return InviteLinksListSection.header.rawValue + case .mainLinkHeader, .mainLink: + return InviteLinksListSection.mainLink.rawValue + case .peersHeader, .peer, .peersInfo: + return InviteLinksListSection.peers.rawValue + } + } + + var stableId: StableId { + switch self { + case .header: + return .index(0) + case .mainLinkHeader: + return .index(1) + case .mainLink: + return .index(2) + case .peersHeader: + return .index(4) + case .peersInfo: + return .index(5) + case let .peer(_, peer, _, _): + return .peer(peer.id) + } + } + + var sortIndex: Int { + switch self { + case .header: + return 0 + case .mainLinkHeader: + return 1 + case .mainLink: + return 2 + case .peersHeader: + return 4 + case let .peer(index, _, _, _): + return 10 + index + case .peersInfo: + return 1000 + } + } + + static func ==(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool { + switch lhs { + case let .header(text): + if case .header(text) = rhs { + return true + } else { + return false + } + case let .mainLinkHeader(text): + if case .mainLinkHeader(text) = rhs { + return true + } else { + return false + } + case let .mainLink(lhsLink, lhsIsGenerating): + if case let .mainLink(rhsLink, rhsIsGenerating) = rhs, lhsLink == rhsLink, lhsIsGenerating == rhsIsGenerating { + return true + } else { + return false + } + case let .peersHeader(text): + if case .peersHeader(text) = rhs { + return true + } else { + return false + } + case let .peersInfo(text): + if case .peersInfo(text) = rhs { + return true + } else { + return false + } + case let .peer(index, peer, isSelected, isEnabled): + if case .peer(index, peer, isSelected, isEnabled) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool { + return lhs.sortIndex < rhs.sortIndex + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! FolderInviteLinkListControllerArguments + switch self { + case let .header(text): + return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListNewFolder", sectionId: self.section) + 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: { + if let link { + arguments.copyLink(link.link) + } + }, shareAction: { + if let link { + arguments.shareMainLink(link.link) + } else { + arguments.generateLink() + } + }, contextAction: { node, gesture in + arguments.mainLinkContextAction(link, node, gesture) + }, viewAction: { + if let link { + arguments.openMainLink(link.link) + } + }) + case let .peersHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .peersInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .peer(_, peer, isSelected, isEnabled): + //TODO:localize + return ItemListPeerItem( + presentationData: presentationData, + dateTimeFormat: PresentationDateTimeFormat(), + nameDisplayOrder: presentationData.nameDisplayOrder, + context: arguments.context, + peer: peer, + presence: nil, + text: .text(isEnabled ? "you can invite others here" : "you can't invite others here", .secondary), + label: .none, + editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), + switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled), + enabled: true, + selectable: true, + sectionId: self.section, + action: { + arguments.peerAction(peer, isEnabled) + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + removePeer: { _ in + } + ) + } + } +} + +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, + allPeers: [EnginePeer] +) -> [InviteLinksListEntry] { + var entries: [InviteLinksListEntry] = [] + + 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." + peersHeaderString = "CHATS" + } else if state.selectedPeerIds.count == 1 { + chatCountString = "Anyone with this link can add Gaming Club 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." + 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)) + + entries.append(.peersHeader(peersHeaderString)) + + var sortedPeers: [EnginePeer] = [] + for peer in allPeers.filter({ canShareLinkToPeer(peer: $0) }) { + sortedPeers.append(peer) + } + for peer in allPeers.filter({ !canShareLinkToPeer(peer: $0) }) { + sortedPeers.append(peer) + } + + for peer in sortedPeers { + let isEnabled = canShareLinkToPeer(peer: peer) + entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled)) + } + + return entries +} + +private struct FolderInviteLinkListControllerState: Equatable { + var currentLink: ExportedChatFolderLink? + var selectedPeerIds = Set() + var generatingLink: Bool = false +} + +public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filterId: Int32, 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 dismissTooltipsImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + var initialState = FolderInviteLinkListControllerState() + initialState.currentLink = currentInvitation + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((FolderInviteLinkListControllerState) -> FolderInviteLinkListControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + let _ = updateState + + let revokeLinkDisposable = MetaDisposable() + actionsDisposable.add(revokeLinkDisposable) + + let deleteAllRevokedLinksDisposable = MetaDisposable() + actionsDisposable.add(deleteAllRevokedLinksDisposable) + + var getControllerImpl: (() -> ViewController?)? + + var displayTooltipImpl: ((UndoOverlayContent) -> Void)? + + let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in + let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData) + shareController.completed = { peerIds in + let _ = (context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).start(next: { peerList in + let peers = peerList.compactMap { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId { + text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), nil) + }) + } + shareController.actionCompleted = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + presentControllerImpl?(shareController, nil) + }, openMainLink: { _ in + }, copyLink: { link in + UIPasteboard.general.string = link + + dismissTooltipsImpl?() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, mainLinkContextAction: { invite, node, gesture in + guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?(), let invite = invite else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + dismissTooltipsImpl?() + + UIPasteboard.general.string = invite.link + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .chatFolder(slug: invite.slug)), nil) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { _, f in + f(.dismissWithoutContent) + + let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, revoke: true) + |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.revokeChatFolderLink(filterId: filterId, link: invite) + |> deliverOnMainQueue).start(completed: { + linkUpdated(nil) + dismissImpl?() + }) + }) + }))) + + 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 { + updateState { state in + var state = state + + if state.selectedPeerIds.contains(peer.id) { + state.selectedPeerIds.remove(peer.id) + } else { + state.selectedPeerIds.insert(peer.id) + } + + return state + } + } else { + //TODO:localized + 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." + default: + break + } + dismissTooltipsImpl?() + displayTooltipImpl?(.peers(context: context, peers: [peer], title: nil, text: text, customUndoText: nil)) + } + }, generateLink: { + let currentState = stateValue.with({ $0 }) + if !currentState.generatingLink { + updateState { state in + var state = state + + state.generatingLink = true + + return state + } + + actionsDisposable.add((context.engine.peers.exportChatFolder(filterId: filterId, title: "", peerIds: Array(currentState.selectedPeerIds)) + |> deliverOnMainQueue).start(next: { result in + linkUpdated(result) + + updateState { state in + var state = state + + state.generatingLink = false + state.currentLink = result + + return state + } + }, error: { _ in + })) + } + }) + + var combinedPeerIds: [EnginePeer.Id] = [] + if let currentInvitation { + for peerId in currentInvitation.peerIds { + if !combinedPeerIds.contains(peerId) { + combinedPeerIds.append(peerId) + } + } + } + for peerId in allPeerIds { + if !combinedPeerIds.contains(peerId) { + combinedPeerIds.append(peerId) + } + } + + let allPeers = context.engine.data.subscribe( + EngineDataList(combinedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + + let _ = (allPeers + |> take(1) + |> deliverOnMainQueue).start(next: { peers in + updateState { state in + var state = state + + if let currentInvitation { + for peerId in currentInvitation.peerIds { + state.selectedPeerIds.insert(peerId) + } + } else { + for peerId in allPeerIds { + if let peer = peers.first(where: { $0?.id == peerId }), let peerValue = peer { + if canShareLinkToPeer(peer: peerValue) { + state.selectedPeerIds.insert(peerId) + } + } + } + } + + return state + } + }) + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + allPeers + ) + |> map { presentationData, state, allPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in + let crossfade = false + let animateChanges = false + + //TODO:localize + let title: ItemListControllerTitle + title = .text("Share Folder") + + 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?() + }) + + 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, + allPeers: allPeers.compactMap { $0 } + ), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal + controller.willDisappear = { _ in + dismissTooltipsImpl?() + } + controller.didDisappear = { [weak controller] _ in + controller?.clearItemNodesHighlight(animated: true) + } + controller.visibleBottomContentOffsetChanged = { offset in + if case let .known(value) = offset, value < 40.0 { + + } + } + pushControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.pushViewController(c, animated: true) + } + } + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window(.root), with: p) + } + } + presentInGlobalOverlayImpl = { [weak controller] c in + if let controller = controller { + controller.presentInGlobalOverlay(c) + } + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } + getControllerImpl = { [weak controller] in + return controller + } + displayTooltipImpl = { [weak controller] c in + if let controller = controller { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current) + } + } + dismissTooltipsImpl = { [weak controller] in + controller?.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + controller?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + } + return controller +} diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift new file mode 100644 index 0000000000..ad37543a60 --- /dev/null +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift @@ -0,0 +1,593 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import ItemListUI +import SolidRoundedButtonNode +import AnimatedAvatarSetNode +import ShimmerEffect +import TelegramCore + +private func actionButtonImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setBlendMode(.clear) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0))) + }) +} + +public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let invite: ExportedChatFolderLink? + let count: Int32 + let peers: [EnginePeer] + let displayButton: Bool + let enableButton: Bool + let buttonTitle: String + let displayImporters: Bool + let buttonColor: UIColor? + public let sectionId: ItemListSectionId + let style: ItemListStyle + let copyAction: (() -> Void)? + let shareAction: (() -> Void)? + let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + let viewAction: (() -> Void)? + public let tag: ItemListItemTag? + + public init( + context: AccountContext, + presentationData: ItemListPresentationData, + invite: ExportedChatFolderLink?, + count: Int32, + peers: [EnginePeer], + displayButton: Bool, + enableButton: Bool, + buttonTitle: String, + displayImporters: Bool, + buttonColor: UIColor?, + sectionId: ItemListSectionId, + style: ItemListStyle, + copyAction: (() -> Void)?, + shareAction: (() -> Void)?, + contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, + viewAction: (() -> Void)?, + tag: ItemListItemTag? = nil + ) { + self.context = context + self.presentationData = presentationData + self.invite = invite + self.count = count + self.peers = peers + self.displayButton = displayButton + self.enableButton = enableButton + self.buttonTitle = buttonTitle + self.displayImporters = displayImporters + self.buttonColor = buttonColor + self.sectionId = sectionId + self.style = style + self.copyAction = copyAction + self.shareAction = shareAction + self.contextAction = contextAction + self.viewAction = viewAction + self.tag = tag + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ItemListFolderInviteLinkItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListFolderInviteLinkItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = false +} + +public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let fieldNode: ASImageNode + private let addressNode: TextNode + private let fieldButtonNode: HighlightTrackingButtonNode + private let referenceContainerNode: ContextReferenceContentNode + private let containerNode: ContextControllerSourceNode + private let addressButtonNode: HighlightTrackingButtonNode + private let addressButtonIconNode: ASImageNode + private var addressShimmerNode: ShimmerEffectNode? + private var shareButtonNode: SolidRoundedButtonNode? + + private let avatarsButtonNode: HighlightTrackingButtonNode + private let avatarsContext: AnimatedAvatarSetContext + private var avatarsContent: AnimatedAvatarSetContext.Content? + private let avatarsNode: AnimatedAvatarSetNode + private let invitedPeersNode: TextNode + private var shimmerNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + + private let activateArea: AccessibilityAreaNode + + private var item: ItemListFolderInviteLinkItem? + + override public var canBeSelected: Bool { + return false + } + + public var tag: ItemListItemTag? { + return self.item?.tag + } + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.fieldNode = ASImageNode() + self.fieldNode.displaysAsynchronously = false + self.fieldNode.displayWithoutProcessing = true + + self.addressNode = TextNode() + self.addressNode.isUserInteractionEnabled = false + + self.fieldButtonNode = HighlightTrackingButtonNode() + + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + self.referenceContainerNode = ContextReferenceContentNode() + + self.addressButtonNode = HighlightTrackingButtonNode() + self.addressButtonIconNode = ASImageNode() + self.addressButtonIconNode.contentMode = .center + self.addressButtonIconNode.displaysAsynchronously = false + self.addressButtonIconNode.displayWithoutProcessing = true + + self.avatarsButtonNode = HighlightTrackingButtonNode() + self.avatarsContext = AnimatedAvatarSetContext() + self.avatarsNode = AnimatedAvatarSetNode() + self.invitedPeersNode = TextNode() + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.fieldNode) + self.addSubnode(self.addressNode) + self.addSubnode(self.fieldButtonNode) + self.addSubnode(self.avatarsNode) + self.addSubnode(self.invitedPeersNode) + self.addSubnode(self.avatarsButtonNode) + + self.containerNode.addSubnode(self.referenceContainerNode) + self.referenceContainerNode.addSubnode(self.addressButtonIconNode) + self.referenceContainerNode.addSubnode(self.addressButtonNode) + self.addSubnode(self.containerNode) + + self.addSubnode(self.activateArea) + + self.containerNode.activated = { [weak self] gesture, _ in + if let strongSelf = self, let item = strongSelf.item { + item.contextAction?(strongSelf.referenceContainerNode, gesture) + } + } + + self.fieldButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.addressNode.layer.removeAnimation(forKey: "opacity") + strongSelf.addressNode.alpha = 0.4 + } else { + strongSelf.addressNode.alpha = 1.0 + strongSelf.addressNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.fieldButtonNode.addTarget(self, action: #selector(self.fieldButtonPressed), forControlEvents: .touchUpInside) + + self.addressButtonNode.addTarget(self, action: #selector(self.addressButtonPressed), forControlEvents: .touchUpInside) + self.addressButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.addressButtonIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.addressButtonIconNode.alpha = 0.4 + } else { + strongSelf.addressButtonIconNode.alpha = 1.0 + strongSelf.addressButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.shareButtonNode?.pressed = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + item.shareAction?() + } + } + self.avatarsButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.avatarsNode.layer.removeAnimation(forKey: "opacity") + strongSelf.invitedPeersNode.layer.removeAnimation(forKey: "opacity") + strongSelf.avatarsNode.alpha = 0.4 + strongSelf.invitedPeersNode.alpha = 0.4 + } else { + strongSelf.avatarsNode.alpha = 1.0 + strongSelf.invitedPeersNode.alpha = 1.0 + strongSelf.avatarsNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.invitedPeersNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.avatarsButtonNode.addTarget(self, action: #selector(self.avatarsButtonPressed), forControlEvents: .touchUpInside) + } + + @objc private func fieldButtonPressed() { + if let item = self.item { + item.copyAction?() + } + } + + @objc private func addressButtonPressed() { + if let item = self.item { + item.contextAction?(self.referenceContainerNode, nil) + } + } + + @objc private func avatarsButtonPressed() { + if let item = self.item { + item.viewAction?() + } + } + + public func asyncLayout() -> (_ item: ItemListFolderInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeAddressLayout = TextNode.asyncLayout(self.addressNode) + let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode) + + let currentItem = self.item + let avatarsContext = self.avatarsContext + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let leftInset = 16.0 + params.leftInset + let rightInset = 16.0 + params.rightInset + + let titleColor: UIColor + titleColor = item.presentationData.theme.list.itemInputField.primaryColor + + let alignCentrally = !"".isEmpty//!(item.invite?.link?.contains("joinchat") ?? true) + + let addressFont = Font.regular(!alignCentrally && params.width == 320 ? floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0) : item.presentationData.fontSize.itemListBaseFontSize) + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + let constrainedWidth = alignCentrally ? params.width - leftInset - rightInset - 90.0 : params.width - leftInset - rightInset - 60.0 + + let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invite.flatMap({ $0.link.replacingOccurrences(of: "https://", with: "") }) ?? "", font: addressFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let subtitle: String + let subtitleColor: UIColor + if item.count > 0 { + subtitle = item.presentationData.strings.InviteLink_PeopleJoined(item.count) + subtitleColor = item.presentationData.theme.list.itemAccentColor + } else { + subtitle = item.presentationData.strings.InviteLink_PeopleJoinedNone + subtitleColor = item.presentationData.theme.list.itemSecondaryTextColor + } + + let (invitedPeersLayout, invitedPeersApply) = makeInvitedPeersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let avatarsContent = avatarsContext.update(peers: item.peers, animated: false) + + let verticalInset: CGFloat = 16.0 + let fieldHeight: CGFloat = 52.0 + let fieldSpacing: CGFloat = 16.0 + let buttonHeight: CGFloat = 50.0 + + var height = verticalInset * 2.0 + fieldHeight + fieldSpacing + buttonHeight + 54.0 + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = .clear + insets = UIEdgeInsets() + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + if !item.displayImporters { + height -= 57.0 + } + if !item.displayButton { + height -= 63.0 + } + + contentSize = CGSize(width: params.width, height: height) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.avatarsContent = avatarsContent + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) +// strongSelf.activateArea.accessibilityLabel = item.title +// strongSelf.activateArea.accessibilityValue = item.label + strongSelf.activateArea.accessibilityTraits = [] + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.fieldNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: item.presentationData.theme.list.itemInputField.backgroundColor) + strongSelf.addressButtonIconNode.image = actionButtonImage(color: item.presentationData.theme.list.itemInputField.controlColor) + } + + let _ = addressApply() + let _ = invitedPeersApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight)) + strongSelf.fieldNode.frame = fieldFrame + strongSelf.fieldButtonNode.frame = fieldFrame + + strongSelf.addressNode.frame = CGRect(origin: CGPoint(x: fieldFrame.minX + (alignCentrally ? floorToScreenPixels((fieldFrame.width - addressLayout.size.width) / 2.0) : 14.0), y: fieldFrame.minY + floorToScreenPixels((fieldFrame.height - addressLayout.size.height) / 2.0) + 1.0), size: addressLayout.size) + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - 38.0 - 14.0, y: verticalInset), size: CGSize(width: 52.0, height: 52.0)) + strongSelf.addressButtonNode.frame = strongSelf.containerNode.bounds + strongSelf.referenceContainerNode.frame = strongSelf.containerNode.bounds + strongSelf.addressButtonIconNode.frame = strongSelf.containerNode.bounds + + let shareButtonNode: SolidRoundedButtonNode + if let currentShareButtonNode = strongSelf.shareButtonNode { + shareButtonNode = currentShareButtonNode + } 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) + } + shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 11.0) + shareButtonNode.pressed = { [weak self] in + self?.item?.shareAction?() + } + strongSelf.addSubnode(shareButtonNode) + strongSelf.shareButtonNode = shareButtonNode + } + + shareButtonNode.title = item.buttonTitle + + let buttonWidth = contentSize.width - leftInset - rightInset + let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate) + shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + + var totalWidth = invitedPeersLayout.size.width + var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0) + let avatarSpacing: CGFloat = 21.0 + if let avatarsContent = strongSelf.avatarsContent { + let avatarsSize = strongSelf.avatarsNode.update(context: item.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true) + + if !avatarsSize.width.isZero { + totalWidth += avatarsSize.width + avatarSpacing + } + + let avatarsNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0), size: avatarsSize) + strongSelf.avatarsNode.frame = avatarsNodeFrame + if !avatarsSize.width.isZero { + leftOrigin = avatarsNodeFrame.maxX + avatarSpacing + } + } + + strongSelf.invitedPeersNode.frame = CGRect(origin: CGPoint(x: leftOrigin, y: fieldFrame.maxY + 92.0), size: invitedPeersLayout.size) + + strongSelf.avatarsButtonNode.frame = CGRect(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0, width: totalWidth, height: 32.0) + strongSelf.avatarsButtonNode.isUserInteractionEnabled = !item.peers.isEmpty && item.invite != nil + + strongSelf.addressButtonNode.isUserInteractionEnabled = item.invite != nil + strongSelf.fieldButtonNode.isUserInteractionEnabled = item.invite != nil + strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0 + + strongSelf.shareButtonNode?.isUserInteractionEnabled = item.enableButton + strongSelf.shareButtonNode?.alpha = item.enableButton ? 1.0 : 0.4 + strongSelf.shareButtonNode?.isHidden = !item.displayButton + strongSelf.avatarsButtonNode.isHidden = !item.displayImporters + strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil + strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil + + if item.invite == nil { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.shimmerNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.shimmerNode = shimmerNode + strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.fieldNode) + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + if itemListHasRoundedBlockLayout(params) { + shimmerNode.clipsToBounds = true + shimmerNode.cornerRadius = 11.0 + } else { + shimmerNode.cornerRadius = 0.0 + } + + let lineWidth: CGFloat = 180.0 + let lineDiameter: CGFloat = 12.0 + let titleFrame = strongSelf.invitedPeersNode.frame + + var shapes: [ShimmerEffectNode.Shape] = [] + shapes.append(.roundedRectLine(startPoint: CGPoint(x: floor(titleFrame.center.x - lineWidth / 2.0), y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: lineWidth, diameter: lineDiameter)) + shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize) + + let addressShimmerNode: ShimmerEffectNode + if let current = strongSelf.addressShimmerNode { + addressShimmerNode = current + } else { + addressShimmerNode = ShimmerEffectNode() + strongSelf.addressShimmerNode = addressShimmerNode + strongSelf.insertSubnode(addressShimmerNode, aboveSubnode: strongSelf.fieldNode) + } + addressShimmerNode.frame = strongSelf.fieldNode.frame.insetBy(dx: 18.0, dy: 0.0) + if let (rect, size) = strongSelf.absoluteLocation { + addressShimmerNode.updateAbsoluteRect(CGRect(x: rect.minX + strongSelf.fieldNode.frame.minX + 18.0, y: rect.minY + strongSelf.fieldNode.frame.minY, width: strongSelf.fieldNode.frame.width - 18.0 * 2.0, height: strongSelf.fieldNode.frame.height), within: size) + } + + let addressLineWidth: CGFloat = strongSelf.fieldNode.frame.width - 100.0 + var addressShapes: [ShimmerEffectNode.Shape] = [] + addressShapes.append(.roundedRectLine(startPoint: CGPoint(x: floor(addressShimmerNode.frame.width / 2.0 - addressLineWidth / 2.0), y: 16.0 + floor((22.0 - lineDiameter) / 2.0)), width: addressLineWidth, diameter: lineDiameter)) + addressShimmerNode.update(backgroundColor: item.presentationData.theme.list.itemInputField.backgroundColor, foregroundColor: item.presentationData.theme.list.itemInputField.controlColor.mixedWith(item.presentationData.theme.list.itemInputField.backgroundColor, alpha: 0.7), shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: addressShapes, size: addressShimmerNode.frame.size) + + } else { + if let shimmerNode = strongSelf.shimmerNode { + strongSelf.shimmerNode = nil + shimmerNode.removeFromSupernode() + } + if let shimmerNode = strongSelf.addressShimmerNode { + strongSelf.shimmerNode = nil + shimmerNode.removeFromSupernode() + } + } + } + }) + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.addressShimmerNode { + shimmerNode.updateAbsoluteRect(CGRect(x: rect.minX + self.fieldNode.frame.minX + 18.0, y: rect.minY + self.fieldNode.frame.minY, width: self.fieldNode.frame.width - 18.0 * 2.0, height: self.fieldNode.frame.height), within: containerSize) + } + if let shimmerNode = self.shimmerNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } +} diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift new file mode 100644 index 0000000000..0547c76ede --- /dev/null +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift @@ -0,0 +1,757 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ItemListUI +import ShimmerEffect +import TelegramCore + +private enum ItemBackgroundColor: Equatable { + case blue + case green + case yellow + case red + case gray + + var colors: (top: UIColor, bottom: UIColor, text: UIColor) { + switch self { + case .blue: + return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff)) + case .green: + return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6)) + case .yellow: + return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7)) + case .red: + return (UIColor(rgb: 0xf2656a), UIColor(rgb: 0xf25f65), UIColor(rgb: 0xffd3de)) + case .gray: + return (UIColor(rgb: 0xa8b2bb), UIColor(rgb: 0xa2abb4), UIColor(rgb: 0xe3e6e8)) + } + } +} + +public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let invite: ExportedChatFolderLink? + let share: Bool + public let sectionId: ItemListSectionId + let style: ItemListStyle + let tapAction: ((ExportedChatFolderLink) -> Void)? + let contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)? + public let tag: ItemListItemTag? + + public init( + presentationData: ItemListPresentationData, + invite: ExportedChatFolderLink?, + share: Bool, + sectionId: ItemListSectionId, + style: ItemListStyle, + tapAction: ((ExportedChatFolderLink) -> Void)?, + contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?, + tag: ItemListItemTag? = nil + ) { + self.presentationData = presentationData + self.invite = invite + self.share = share + self.sectionId = sectionId + self.style = style + self.tapAction = tapAction + self.contextAction = contextAction + self.tag = tag + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } + if nextItem == nil { + last = true + } + } + let node = ItemListFolderInviteLinkListItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListFolderInviteLinkListItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } + if nextItem == nil { + last = true + } + } + + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + if let invite = self.invite { + self.tapAction?(invite) + } + } +} + +public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let extractedBackgroundImageNode: ASImageNode + + private let containerNode: ContextControllerSourceNode + private let contextSourceNode: ContextExtractedContentContainingNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let offsetContainerNode: ASDisplayNode + + private let iconBackgroundNode: ASDisplayNode + private let iconNode: ASImageNode + private var timerNode: TimerNode? + + private let titleNode: TextNode + private let subtitleNode: TextNode + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + + private var currentColor: ItemBackgroundColor? + private var layoutParams: (ItemListFolderInviteLinkListItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? + + public var tag: ItemListItemTag? + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + self.maskNode = ASImageNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.offsetContainerNode = ASDisplayNode() + + self.iconBackgroundNode = ASDisplayNode() + self.iconBackgroundNode.setLayerBlock { () -> CALayer in + return CAShapeLayer() + } + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.contentMode = .center + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.contentMode = .left + self.subtitleNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.isAccessibilityElement = true + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) + + self.offsetContainerNode.addSubnode(self.iconBackgroundNode) + self.offsetContainerNode.addSubnode(self.iconNode) + self.offsetContainerNode.addSubnode(self.titleNode) + self.offsetContainerNode.addSubnode(self.subtitleNode) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let invite = item.invite, let contextAction = item.contextAction else { + gesture.cancel() + return + } + contextAction(invite, strongSelf.contextSourceNode, gesture) + } + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.plainBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } + } + + public override func didLoad() { + super.didLoad() + + if let shapeLayer = self.iconBackgroundNode.layer as? CAShapeLayer { + shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)).cgPath + } + } + + public func asyncLayout() -> (_ item: ItemListFolderInviteLinkListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors, firstWithHeader, last in + var updatedTheme: PresentationTheme? + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let color: ItemBackgroundColor + let nextColor: ItemBackgroundColor + let transitionFraction: CGFloat + + color = .blue + nextColor = .blue + transitionFraction = 1.0 + + let topColor = color.colors.top + let nextTopColor = nextColor.colors.top + let iconColor: UIColor + if let _ = item.invite { + if case .blue = color { + iconColor = item.presentationData.theme.list.itemAccentColor + } else { + iconColor = nextTopColor.mixedWith(topColor, alpha: transitionFraction) + } + } else { + iconColor = item.presentationData.theme.list.mediaPlaceholderColor + } + + let inviteLink = item.invite?.link.replacingOccurrences(of: "https://", with: "") ?? "" + var titleText = inviteLink + var subtitleText: String = "" + + if let invite = item.invite { + if !invite.title.isEmpty { + titleText = invite.title + } + + //TODO:localize + if invite.peerIds.count == 1 { + subtitleText = "includes 1 chat" + } else { + subtitleText = "includes \(invite.peerIds.count) chats" + } + } else { + titleText = " " + subtitleText = " " + } + + let titleAttributedString = NSAttributedString(string: titleText, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let subtitleAttributedString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + + let leftInset: CGFloat = 65.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + let verticalInset: CGFloat = subtitleAttributedString.string.isEmpty ? 14.0 : 8.0 + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = 1.0 + + let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0 + let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + + var insets: UIEdgeInsets + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + insets.top = firstWithHeader ? 29.0 : 0.0 + insets.bottom = 0.0 + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight)) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.layoutParams = (item, params, neighbors, firstWithHeader, last) + + strongSelf.accessibilityLabel = titleAttributedString.string + strongSelf.accessibilityValue = subtitleAttributedString.string + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.isGestureEnabled = item.contextAction != nil + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) + let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + strongSelf.extractedRect = extractedRect + strongSelf.nonExtractedRect = nonExtractedRect + + if strongSelf.contextSourceNode.isExtractedToContextPreview { + strongSelf.extractedBackgroundImageNode.frame = extractedRect + } else { + strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect + } + strongSelf.contextSourceNode.contentRect = extractedRect + + if let layer = strongSelf.iconBackgroundNode.layer as? CAShapeLayer { + layer.fillColor = iconColor.cgColor + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } + + let transition = ContainedViewLayoutTransition.immediate + + let _ = titleApply() + let _ = subtitleApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + + let stripeInset: CGFloat + if case .none = neighbors.bottom { + stripeInset = 0.0 + } else { + stripeInset = leftInset + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: stripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - stripeInset, height: separatorHeight)) + strongSelf.bottomStripeNode.isHidden = last + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + let iconSize: CGSize = CGSize(width: 40.0, height: 40.0) + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize) + strongSelf.iconBackgroundNode.bounds = CGRect(origin: CGPoint(), size: iconSize) + strongSelf.iconBackgroundNode.position = iconFrame.center + strongSelf.iconNode.frame = iconFrame + + transition.updateTransformScale(node: strongSelf.iconBackgroundNode, scale: 1.0) + + strongSelf.timerNode?.frame = iconFrame.insetBy(dx: -5.0, dy: -5.0) + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + + if item.invite == nil { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + strongSelf.addSubnode(shimmerNode) + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = 180.0 + let subtitleLineWidth: CGFloat = 60.0 + let lineDiameter: CGFloat = 10.0 + + let iconFrame = strongSelf.iconBackgroundNode.frame + shapes.append(.circle(iconFrame)) + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + let subtitleFrame = strongSelf.subtitleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter)) + + shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize) + } else if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } + } + }) + } + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } +} + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +private final class TimerNode: ASDisplayNode { + enum Value: Equatable { + case timestamp(creation: Int32, deadline: Int32) + case fraction(CGFloat) + } + private struct Params: Equatable { + var color: UIColor + var value: Value + } + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var inHierarchyValue: Bool = false + + private var animator: ConstantDisplayLinkAnimator? + private let contentNode: ASDisplayNode + private var particles: [ContentParticle] = [] + + private var currentParams: Params? + + var reachedTimeout: (() -> Void)? + + override init() { + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + self.contentNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.contentNode) + + updateInHierarchy = { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.inHierarchyValue = value + strongSelf.animator?.isPaused = value + } + } + + deinit { + self.animator?.invalidate() + } + + func update(color: UIColor, value: Value) { + let params = Params( + color: color, + value: value + ) + self.currentParams = params + + self.updateValues() + } + + private func updateValues() { + guard let params = self.currentParams else { + return + } + + let color = params.color + + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var fraction: CGFloat + switch params.value { + case let .fraction(value): + fraction = value + case let .timestamp(creation, deadline): + fraction = CGFloat(deadline - currentTimestamp) / CGFloat(deadline - creation) + } + fraction = max(0.0001, 1.0 - max(0.0, min(1.0, fraction))) + + let image: UIImage? + + let diameter: CGFloat = 42.0 + let inset: CGFloat = 8.0 + let lineWidth: CGFloat = 2.0 + + let timestamp = CACurrentMediaTime() + + let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0) + let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0 + + let startAngle: CGFloat = -CGFloat.pi / 2.0 + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction + + let sparks = fraction > 0.05 && fraction != 1.0 + if sparks { + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) + let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3 + + let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + } + + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + + if sparks { + for particle in self.particles { + let size: CGFloat = 2.0 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + } + }) + + self.contentNode.contents = image?.cgImage + if let image = image { + self.contentNode.frame = CGRect(origin: CGPoint(), size: image.size) + } + + if fraction <= .ulpOfOne { + self.animator?.invalidate() + self.animator = nil + } else { + if self.animator == nil { + let animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateValues() + }) + self.animator = animator + animator.isPaused = self.inHierarchyValue + } + } + } +} diff --git a/submodules/ItemListPeerItem/BUILD b/submodules/ItemListPeerItem/BUILD index a2af81dd51..784835efe7 100644 --- a/submodules/ItemListPeerItem/BUILD +++ b/submodules/ItemListPeerItem/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", + "//submodules/CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 06471c28f0..f8d1a08c53 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -16,6 +16,7 @@ import ContextUI import AccountContext import ComponentFlow import EmojiStatusComponent +import CheckNode private final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? @@ -252,16 +253,19 @@ public enum ItemListPeerItemLabel { public struct ItemListPeerItemSwitch { public var value: Bool public var style: ItemListPeerItemSwitchStyle + public var isEnabled: Bool - public init(value: Bool, style: ItemListPeerItemSwitchStyle) { + public init(value: Bool, style: ItemListPeerItemSwitchStyle, isEnabled: Bool = true) { self.value = value self.style = style + self.isEnabled = isEnabled } } public enum ItemListPeerItemSwitchStyle { case standard case check + case leftCheck } public enum ItemListPeerItemAliasHandling { @@ -474,6 +478,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private var credibilityIconView: ComponentHostView? private var switchNode: SwitchNode? private var checkNode: ASImageNode? + private var leftCheckNode: CheckNode? private var shimmerNode: LoadingShimmerNode? private var absoluteLocation: (CGRect, CGSize)? @@ -736,6 +741,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo peerRevealOptions = [] } + var additionalLeftInset: CGFloat = 0.0 + var leftInset: CGFloat = params.leftInset var rightInset: CGFloat = params.rightInset let switchSize = CGSize(width: 51.0, height: 31.0) var checkImage: UIImage? @@ -755,6 +762,11 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } rightInset += 24.0 currentSwitchNode = nil + case .leftCheck: + additionalLeftInset += 40.0 + leftInset += additionalLeftInset + currentSwitchNode = nil + currentCheckNode = nil } } else { currentSwitchNode = nil @@ -842,7 +854,6 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo break } - let leftInset: CGFloat let verticalInset: CGFloat let verticalOffset: CGFloat let avatarSize: CGFloat @@ -856,7 +867,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } verticalOffset = 0.0 avatarSize = 31.0 - leftInset = 59.0 + params.leftInset + leftInset += 59.0 avatarFontSize = floor(31.0 * 16.0 / 37.0) case .peerList: if case .none = item.text { @@ -866,7 +877,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } verticalOffset = 0.0 avatarSize = 40.0 - leftInset = 65.0 + params.leftInset + leftInset += 65.0 avatarFontSize = floor(40.0 * 16.0 / 37.0) } @@ -1234,9 +1245,29 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) - let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + additionalLeftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) + if let switchValue = item.switchValue, case .leftCheck = switchValue.style { + let leftCheckNode: CheckNode + if let current = strongSelf.leftCheckNode { + leftCheckNode = current + } else { + var checkTheme = CheckNodeTheme(theme: item.presentationData.theme, style: .plain) + checkTheme.isDottedBorder = !switchValue.isEnabled + leftCheckNode = CheckNode(theme: checkTheme) + strongSelf.leftCheckNode = leftCheckNode + strongSelf.avatarNode.supernode?.addSubnode(leftCheckNode) + } + leftCheckNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: floor((layout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0)) + leftCheckNode.setSelected(switchValue.value, animated: animated) + } else { + if let leftCheckNode = strongSelf.leftCheckNode { + strongSelf.leftCheckNode = nil + leftCheckNode.removeFromSupernode() + } + } + if let threadInfo = item.threadInfo { let threadIconSize = floor(avatarSize * 0.9) let threadIconFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - threadIconSize) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - threadIconSize) / 2.0)), size: CGSize(width: threadIconSize, height: threadIconSize)) diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index cd5e3546d4..dcf605ce57 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -38,6 +38,7 @@ public final class QrCodeScreen: ViewController { public enum Subject { case peer(peer: EnginePeer) case invite(invite: ExportedInvitation, isGroup: Bool) + case chatFolder(slug: String) var link: String { switch self { @@ -45,6 +46,8 @@ public final class QrCodeScreen: ViewController { return "https://t.me/\(peer.addressName ?? "")" case let .invite(invite, _): return invite.link ?? "" + case let .chatFolder(slug): + return "https://t.me/folder/\(slug)" } } @@ -54,6 +57,8 @@ public final class QrCodeScreen: ViewController { return "Q" case .invite: return "Q" + case .chatFolder: + return "Q" } } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4ab41c4447..10da945042 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -3,6 +3,8 @@ public enum Api { public enum account {} public enum auth {} public enum channels {} + public enum communities {} + public enum community {} public enum contacts {} public enum help {} public enum messages {} @@ -20,6 +22,7 @@ public enum Api { public enum auth {} public enum bots {} public enum channels {} + public enum communities {} public enum contacts {} public enum folders {} public enum help {} @@ -190,6 +193,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-712374074] = { return Api.Dialog.parse_dialog($0) } dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) } dict[1949890536] = { return Api.DialogFilter.parse_dialogFilter($0) } + dict[-665432009] = { return Api.DialogFilter.parse_dialogFilterCommunity($0) } dict[909284270] = { return Api.DialogFilter.parse_dialogFilterDefault($0) } dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) } dict[-445792507] = { return Api.DialogPeer.parse_dialogPeer($0) } @@ -234,6 +238,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[594758406] = { return Api.EncryptedMessage.parse_encryptedMessageService($0) } dict[179611673] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) } dict[-317687113] = { return Api.ExportedChatInvite.parse_chatInvitePublicJoinRequests($0) } + dict[-337788502] = { return Api.ExportedCommunityInvite.parse_exportedCommunityInvite($0) } dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) } dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) } dict[-207944868] = { return Api.FileHash.parse_fileHash($0) } @@ -285,6 +290,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1736378792] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordEmpty($0) } dict[-763367294] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordSRP($0) } dict[1968737087] = { return Api.InputClientProxy.parse_inputClientProxy($0) } + dict[450955169] = { return Api.InputCommunity.parse_inputCommunityDialogFilter($0) } dict[-208488460] = { return Api.InputContact.parse_inputPhoneContact($0) } dict[-55902537] = { return Api.InputDialogPeer.parse_inputDialogPeer($0) } dict[1684014375] = { return Api.InputDialogPeer.parse_inputDialogPeerFolder($0) } @@ -456,7 +462,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) } dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } - dict[-1415514682] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) } dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) } @@ -990,6 +996,10 @@ 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[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) } @@ -1341,6 +1351,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.ExportedChatInvite: _1.serialize(buffer, boxed) + case let _1 as Api.ExportedCommunityInvite: + _1.serialize(buffer, boxed) case let _1 as Api.ExportedContactToken: _1.serialize(buffer, boxed) case let _1 as Api.ExportedMessageLink: @@ -1397,6 +1409,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputClientProxy: _1.serialize(buffer, boxed) + case let _1 as Api.InputCommunity: + _1.serialize(buffer, boxed) case let _1 as Api.InputContact: _1.serialize(buffer, boxed) case let _1 as Api.InputDialogPeer: @@ -1783,6 +1797,12 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.channels.SendAsPeers: _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/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 7ca130b632..52955dbb5a 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -247,7 +247,7 @@ public extension Api { case messageActionEmpty case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) - case messageActionGiftPremium(currency: String, amount: Int64, months: Int32) + case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?) case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionHistoryClear @@ -387,13 +387,16 @@ public extension Api { toId.serialize(buffer, true) serializeInt32(distance, buffer: buffer, boxed: false) break - case .messageActionGiftPremium(let currency, let amount, let months): + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): if boxed { - buffer.appendInt32(-1415514682) + buffer.appendInt32(-935499028) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) serializeInt32(months, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} break case .messageActionGroupCall(let flags, let call, let duration): if boxed { @@ -588,8 +591,8 @@ public extension Api { return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) case .messageActionGeoProximityReached(let fromId, let toId, let distance): return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) - case .messageActionGiftPremium(let currency, let amount, let months): - return ("messageActionGiftPremium", [("currency", currency as Any), ("amount", amount as Any), ("months", months as Any)]) + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): + return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) case .messageActionGroupCall(let flags, let call, let duration): return ("messageActionGroupCall", [("flags", flags as Any), ("call", call as Any), ("duration", duration as Any)]) case .messageActionGroupCallScheduled(let call, let scheduleDate): @@ -820,17 +823,26 @@ public extension Api { } } public static func parse_messageActionGiftPremium(_ reader: BufferReader) -> MessageAction? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } + var _6: Int64? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.MessageAction.messageActionGiftPremium(currency: _1!, amount: _2!, months: _3!) + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index a008cd6877..b9a765e778 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -850,6 +850,228 @@ public extension Api.channels { } } +public extension Api.communities { + enum ExportedCommunityInvite: TypeConstructorDescription { + case exportedCommunityInvite(filter: Api.DialogFilter, invite: Api.ExportedCommunityInvite) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedCommunityInvite(let filter, let invite): + if boxed { + buffer.appendInt32(1805101290) + } + filter.serialize(buffer, true) + invite.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedCommunityInvite(let filter, let invite): + return ("exportedCommunityInvite", [("filter", filter as Any), ("invite", invite as Any)]) + } + } + + public static func parse_exportedCommunityInvite(_ reader: BufferReader) -> ExportedCommunityInvite? { + var _1: Api.DialogFilter? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.DialogFilter + } + var _2: Api.ExportedCommunityInvite? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.ExportedCommunityInvite + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.communities.ExportedCommunityInvite.exportedCommunityInvite(filter: _1!, invite: _2!) + } + else { + return nil + } + } + + } +} +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 + } + } + + } +} public extension Api.contacts { enum Blocked: TypeConstructorDescription { case blocked(blocked: [Api.PeerBlocked], chats: [Api.Chat], users: [Api.User]) @@ -962,143 +1184,3 @@ public extension Api.contacts { } } -public extension Api.contacts { - enum Contacts: TypeConstructorDescription { - case contacts(contacts: [Api.Contact], savedCount: Int32, users: [Api.User]) - case contactsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .contacts(let contacts, let savedCount, let users): - if boxed { - buffer.appendInt32(-353862078) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(contacts.count)) - for item in contacts { - item.serialize(buffer, true) - } - serializeInt32(savedCount, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .contactsNotModified: - if boxed { - buffer.appendInt32(-1219778094) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .contacts(let contacts, let savedCount, let users): - return ("contacts", [("contacts", contacts as Any), ("savedCount", savedCount as Any), ("users", users as Any)]) - case .contactsNotModified: - return ("contactsNotModified", []) - } - } - - public static func parse_contacts(_ reader: BufferReader) -> Contacts? { - var _1: [Api.Contact]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Contact.self) - } - var _2: Int32? - _2 = reader.readInt32() - 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.contacts.Contacts.contacts(contacts: _1!, savedCount: _2!, users: _3!) - } - else { - return nil - } - } - public static func parse_contactsNotModified(_ reader: BufferReader) -> Contacts? { - return Api.contacts.Contacts.contactsNotModified - } - - } -} -public extension Api.contacts { - enum Found: TypeConstructorDescription { - case found(myResults: [Api.Peer], results: [Api.Peer], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .found(let myResults, let results, let chats, let users): - if boxed { - buffer.appendInt32(-1290580579) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(myResults.count)) - for item in myResults { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(results.count)) - for item in results { - 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 .found(let myResults, let results, let chats, let users): - return ("found", [("myResults", myResults as Any), ("results", results as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_found(_ reader: BufferReader) -> Found? { - var _1: [Api.Peer]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - 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.contacts.Found.found(myResults: _1!, results: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 2cd6480b05..1a273cf5cc 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1,3 +1,143 @@ +public extension Api.contacts { + enum Contacts: TypeConstructorDescription { + case contacts(contacts: [Api.Contact], savedCount: Int32, users: [Api.User]) + case contactsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .contacts(let contacts, let savedCount, let users): + if boxed { + buffer.appendInt32(-353862078) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(contacts.count)) + for item in contacts { + item.serialize(buffer, true) + } + serializeInt32(savedCount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .contactsNotModified: + if boxed { + buffer.appendInt32(-1219778094) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .contacts(let contacts, let savedCount, let users): + return ("contacts", [("contacts", contacts as Any), ("savedCount", savedCount as Any), ("users", users as Any)]) + case .contactsNotModified: + return ("contactsNotModified", []) + } + } + + public static func parse_contacts(_ reader: BufferReader) -> Contacts? { + var _1: [Api.Contact]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Contact.self) + } + var _2: Int32? + _2 = reader.readInt32() + 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.contacts.Contacts.contacts(contacts: _1!, savedCount: _2!, users: _3!) + } + else { + return nil + } + } + public static func parse_contactsNotModified(_ reader: BufferReader) -> Contacts? { + return Api.contacts.Contacts.contactsNotModified + } + + } +} +public extension Api.contacts { + enum Found: TypeConstructorDescription { + case found(myResults: [Api.Peer], results: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .found(let myResults, let results, let chats, let users): + if boxed { + buffer.appendInt32(-1290580579) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(myResults.count)) + for item in myResults { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(results.count)) + for item in results { + 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 .found(let myResults, let results, let chats, let users): + return ("found", [("myResults", myResults as Any), ("results", results as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_found(_ reader: BufferReader) -> Found? { + var _1: [Api.Peer]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + 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.contacts.Found.found(myResults: _1!, results: _2!, chats: _3!, users: _4!) + } + else { + return nil + } + } + + } +} public extension Api.contacts { enum ImportedContacts: TypeConstructorDescription { case importedContacts(imported: [Api.ImportedContact], popularInvites: [Api.PopularContact], retryContacts: [Int64], users: [Api.User]) @@ -1190,141 +1330,3 @@ public extension Api.help { } } -public extension Api.messages { - enum AffectedFoundMessages: TypeConstructorDescription { - case affectedFoundMessages(pts: Int32, ptsCount: Int32, offset: Int32, messages: [Int32]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .affectedFoundMessages(let pts, let ptsCount, let offset, let messages): - if boxed { - buffer.appendInt32(-275956116) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(ptsCount, buffer: buffer, boxed: false) - serializeInt32(offset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - serializeInt32(item, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .affectedFoundMessages(let pts, let ptsCount, let offset, let messages): - return ("affectedFoundMessages", [("pts", pts as Any), ("ptsCount", ptsCount as Any), ("offset", offset as Any), ("messages", messages as Any)]) - } - } - - public static func parse_affectedFoundMessages(_ reader: BufferReader) -> AffectedFoundMessages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Int32]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.AffectedFoundMessages.affectedFoundMessages(pts: _1!, ptsCount: _2!, offset: _3!, messages: _4!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum AffectedHistory: TypeConstructorDescription { - case affectedHistory(pts: Int32, ptsCount: Int32, offset: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .affectedHistory(let pts, let ptsCount, let offset): - if boxed { - buffer.appendInt32(-1269012015) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(ptsCount, buffer: buffer, boxed: false) - serializeInt32(offset, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .affectedHistory(let pts, let ptsCount, let offset): - return ("affectedHistory", [("pts", pts as Any), ("ptsCount", ptsCount as Any), ("offset", offset as Any)]) - } - } - - public static func parse_affectedHistory(_ reader: BufferReader) -> AffectedHistory? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.AffectedHistory.affectedHistory(pts: _1!, ptsCount: _2!, offset: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum AffectedMessages: TypeConstructorDescription { - case affectedMessages(pts: Int32, ptsCount: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .affectedMessages(let pts, let ptsCount): - if boxed { - buffer.appendInt32(-2066640507) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(ptsCount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .affectedMessages(let pts, let ptsCount): - return ("affectedMessages", [("pts", pts as Any), ("ptsCount", ptsCount as Any)]) - } - } - - public static func parse_affectedMessages(_ reader: BufferReader) -> AffectedMessages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.AffectedMessages.affectedMessages(pts: _1!, ptsCount: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index c18340f6c2..f1029eb040 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1,3 +1,141 @@ +public extension Api.messages { + enum AffectedFoundMessages: TypeConstructorDescription { + case affectedFoundMessages(pts: Int32, ptsCount: Int32, offset: Int32, messages: [Int32]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .affectedFoundMessages(let pts, let ptsCount, let offset, let messages): + if boxed { + buffer.appendInt32(-275956116) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(ptsCount, buffer: buffer, boxed: false) + serializeInt32(offset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .affectedFoundMessages(let pts, let ptsCount, let offset, let messages): + return ("affectedFoundMessages", [("pts", pts as Any), ("ptsCount", ptsCount as Any), ("offset", offset as Any), ("messages", messages as Any)]) + } + } + + public static func parse_affectedFoundMessages(_ reader: BufferReader) -> AffectedFoundMessages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Int32]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.AffectedFoundMessages.affectedFoundMessages(pts: _1!, ptsCount: _2!, offset: _3!, messages: _4!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum AffectedHistory: TypeConstructorDescription { + case affectedHistory(pts: Int32, ptsCount: Int32, offset: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .affectedHistory(let pts, let ptsCount, let offset): + if boxed { + buffer.appendInt32(-1269012015) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(ptsCount, buffer: buffer, boxed: false) + serializeInt32(offset, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .affectedHistory(let pts, let ptsCount, let offset): + return ("affectedHistory", [("pts", pts as Any), ("ptsCount", ptsCount as Any), ("offset", offset as Any)]) + } + } + + public static func parse_affectedHistory(_ reader: BufferReader) -> AffectedHistory? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.AffectedHistory.affectedHistory(pts: _1!, ptsCount: _2!, offset: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum AffectedMessages: TypeConstructorDescription { + case affectedMessages(pts: Int32, ptsCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .affectedMessages(let pts, let ptsCount): + if boxed { + buffer.appendInt32(-2066640507) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(ptsCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .affectedMessages(let pts, let ptsCount): + return ("affectedMessages", [("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + } + } + + public static func parse_affectedMessages(_ reader: BufferReader) -> AffectedMessages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.AffectedMessages.affectedMessages(pts: _1!, ptsCount: _2!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum AllStickers: TypeConstructorDescription { case allStickers(hash: Int64, sets: [Api.StickerSet]) @@ -1258,145 +1396,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum ForumTopics: TypeConstructorDescription { - case forumTopics(flags: Int32, count: Int32, topics: [Api.ForumTopic], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], pts: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .forumTopics(let flags, let count, let topics, let messages, let chats, let users, let pts): - if boxed { - buffer.appendInt32(913709011) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(topics.count)) - for item in topics { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { - 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) - } - serializeInt32(pts, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .forumTopics(let flags, let count, let topics, let messages, let chats, let users, let pts): - return ("forumTopics", [("flags", flags as Any), ("count", count as Any), ("topics", topics as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any), ("pts", pts as Any)]) - } - } - - public static func parse_forumTopics(_ reader: BufferReader) -> ForumTopics? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.ForumTopic]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ForumTopic.self) - } - var _4: [Api.Message]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.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) - } - var _7: Int32? - _7 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.messages.ForumTopics.forumTopics(flags: _1!, count: _2!, topics: _3!, messages: _4!, chats: _5!, users: _6!, pts: _7!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum FoundStickerSets: TypeConstructorDescription { - case foundStickerSets(hash: Int64, sets: [Api.StickerSetCovered]) - case foundStickerSetsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .foundStickerSets(let hash, let sets): - if boxed { - buffer.appendInt32(-1963942446) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - case .foundStickerSetsNotModified: - if boxed { - buffer.appendInt32(223655517) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .foundStickerSets(let hash, let sets): - return ("foundStickerSets", [("hash", hash as Any), ("sets", sets as Any)]) - case .foundStickerSetsNotModified: - return ("foundStickerSetsNotModified", []) - } - } - - public static func parse_foundStickerSets(_ reader: BufferReader) -> FoundStickerSets? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.FoundStickerSets.foundStickerSets(hash: _1!, sets: _2!) - } - else { - return nil - } - } - public static func parse_foundStickerSetsNotModified(_ reader: BufferReader) -> FoundStickerSets? { - return Api.messages.FoundStickerSets.foundStickerSetsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 421a977904..597af62556 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,145 @@ +public extension Api.messages { + enum ForumTopics: TypeConstructorDescription { + case forumTopics(flags: Int32, count: Int32, topics: [Api.ForumTopic], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], pts: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .forumTopics(let flags, let count, let topics, let messages, let chats, let users, let pts): + if boxed { + buffer.appendInt32(913709011) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(topics.count)) + for item in topics { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + 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) + } + serializeInt32(pts, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .forumTopics(let flags, let count, let topics, let messages, let chats, let users, let pts): + return ("forumTopics", [("flags", flags as Any), ("count", count as Any), ("topics", topics as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any), ("pts", pts as Any)]) + } + } + + public static func parse_forumTopics(_ reader: BufferReader) -> ForumTopics? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.ForumTopic]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ForumTopic.self) + } + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.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) + } + var _7: Int32? + _7 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.messages.ForumTopics.forumTopics(flags: _1!, count: _2!, topics: _3!, messages: _4!, chats: _5!, users: _6!, pts: _7!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum FoundStickerSets: TypeConstructorDescription { + case foundStickerSets(hash: Int64, sets: [Api.StickerSetCovered]) + case foundStickerSetsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .foundStickerSets(let hash, let sets): + if boxed { + buffer.appendInt32(-1963942446) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + case .foundStickerSetsNotModified: + if boxed { + buffer.appendInt32(223655517) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .foundStickerSets(let hash, let sets): + return ("foundStickerSets", [("hash", hash as Any), ("sets", sets as Any)]) + case .foundStickerSetsNotModified: + return ("foundStickerSetsNotModified", []) + } + } + + public static func parse_foundStickerSets(_ reader: BufferReader) -> FoundStickerSets? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.FoundStickerSets.foundStickerSets(hash: _1!, sets: _2!) + } + else { + return nil + } + } + public static func parse_foundStickerSetsNotModified(_ reader: BufferReader) -> FoundStickerSets? { + return Api.messages.FoundStickerSets.foundStickerSetsNotModified + } + + } +} public extension Api.messages { enum HighScores: TypeConstructorDescription { case highScores(scores: [Api.HighScore], users: [Api.User]) @@ -1368,61 +1510,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum Stickers: TypeConstructorDescription { - case stickers(hash: Int64, stickers: [Api.Document]) - case stickersNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickers(let hash, let stickers): - if boxed { - buffer.appendInt32(816245886) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { - item.serialize(buffer, true) - } - break - case .stickersNotModified: - if boxed { - buffer.appendInt32(-244016606) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickers(let hash, let stickers): - return ("stickers", [("hash", hash as Any), ("stickers", stickers as Any)]) - case .stickersNotModified: - return ("stickersNotModified", []) - } - } - - public static func parse_stickers(_ reader: BufferReader) -> Stickers? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Stickers.stickers(hash: _1!, stickers: _2!) - } - else { - return nil - } - } - public static func parse_stickersNotModified(_ reader: BufferReader) -> Stickers? { - return Api.messages.Stickers.stickersNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 0e32580da6..bea7e93877 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,61 @@ +public extension Api.messages { + enum Stickers: TypeConstructorDescription { + case stickers(hash: Int64, stickers: [Api.Document]) + case stickersNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickers(let hash, let stickers): + if boxed { + buffer.appendInt32(816245886) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + break + case .stickersNotModified: + if boxed { + buffer.appendInt32(-244016606) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickers(let hash, let stickers): + return ("stickers", [("hash", hash as Any), ("stickers", stickers as Any)]) + case .stickersNotModified: + return ("stickersNotModified", []) + } + } + + public static func parse_stickers(_ reader: BufferReader) -> Stickers? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Stickers.stickers(hash: _1!, stickers: _2!) + } + else { + return nil + } + } + public static func parse_stickersNotModified(_ reader: BufferReader) -> Stickers? { + return Api.messages.Stickers.stickersNotModified + } + + } +} public extension Api.messages { enum TranscribedAudio: TypeConstructorDescription { case transcribedAudio(flags: Int32, transcriptionId: Int64, text: String) diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index f1461716a3..002737c88e 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -2911,6 +2911,111 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.communities { + static func checkCommunityInvite(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1753956947) + serializeString(slug, buffer: buffer, boxed: false) + return (FunctionDescription(name: "communities.checkCommunityInvite", parameters: [("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.community.CommunityInvite? in + let reader = BufferReader(buffer) + var result: Api.community.CommunityInvite? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.community.CommunityInvite + } + return result + }) + } +} +public extension Api.functions.communities { + static func deleteExportedInvite(community: Api.InputCommunity, slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-110213610) + community.serialize(buffer, true) + serializeString(slug, buffer: buffer, boxed: false) + return (FunctionDescription(name: "communities.deleteExportedInvite", parameters: [("community", String(describing: community)), ("slug", String(describing: slug))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.communities { + static func editExportedInvite(flags: Int32, community: Api.InputCommunity, slug: String, title: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(873155725) + serializeInt32(flags, buffer: buffer, boxed: false) + community.serialize(buffer, true) + serializeString(slug, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "communities.editExportedInvite", parameters: [("flags", String(describing: flags)), ("community", String(describing: community)), ("slug", String(describing: slug)), ("title", String(describing: title))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ExportedCommunityInvite? in + let reader = BufferReader(buffer) + var result: Api.ExportedCommunityInvite? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.ExportedCommunityInvite + } + return result + }) + } +} +public extension Api.functions.communities { + static func exportCommunityInvite(community: Api.InputCommunity, title: String, peers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1107192281) + community.serialize(buffer, true) + serializeString(title, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "communities.exportCommunityInvite", parameters: [("community", String(describing: community)), ("title", String(describing: title)), ("peers", String(describing: peers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.communities.ExportedCommunityInvite? in + let reader = BufferReader(buffer) + var result: Api.communities.ExportedCommunityInvite? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.communities.ExportedCommunityInvite + } + return result + }) + } +} +public extension Api.functions.communities { + static func getExportedInvites(community: Api.InputCommunity) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1183359901) + community.serialize(buffer, true) + return (FunctionDescription(name: "communities.getExportedInvites", parameters: [("community", String(describing: community))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.communities.ExportedInvites? in + let reader = BufferReader(buffer) + var result: Api.communities.ExportedInvites? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.communities.ExportedInvites + } + return result + }) + } +} +public extension Api.functions.communities { + static func joinCommunityInvite(slug: String, peers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(82835751) + serializeString(slug, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "communities.joinCommunityInvite", parameters: [("slug", String(describing: slug)), ("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/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index fc20fb15d9..39e7c709cb 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -1015,6 +1015,7 @@ public extension Api { public extension Api { enum DialogFilter: TypeConstructorDescription { case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) + case dialogFilterCommunity(flags: Int32, id: Int32, title: String, emoticon: String?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer]) case dialogFilterDefault public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -1043,6 +1044,25 @@ public extension Api { item.serialize(buffer, true) } break + case .dialogFilterCommunity(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers): + if boxed { + buffer.appendInt32(-665432009) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(id, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(pinnedPeers.count)) + for item in pinnedPeers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(includePeers.count)) + for item in includePeers { + item.serialize(buffer, true) + } + break case .dialogFilterDefault: if boxed { buffer.appendInt32(909284270) @@ -1056,6 +1076,8 @@ public extension Api { switch self { case .dialogFilter(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers, let excludePeers): return ("dialogFilter", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any), ("excludePeers", excludePeers as Any)]) + case .dialogFilterCommunity(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers): + return ("dialogFilterCommunity", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any)]) case .dialogFilterDefault: return ("dialogFilterDefault", []) } @@ -1096,6 +1118,36 @@ public extension Api { return nil } } + public static func parse_dialogFilterCommunity(_ reader: BufferReader) -> DialogFilter? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: String? + if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) } + var _5: [Api.InputPeer]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) + } + var _6: [Api.InputPeer]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 25) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.DialogFilter.dialogFilterCommunity(flags: _1!, id: _2!, title: _3!, emoticon: _4, pinnedPeers: _5!, includePeers: _6!) + } + else { + return nil + } + } public static func parse_dialogFilterDefault(_ reader: BufferReader) -> DialogFilter? { return Api.DialogFilter.dialogFilterDefault } diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 1d327a9163..7731114f90 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -1024,6 +1024,60 @@ public extension Api { } } +public extension Api { + enum ExportedCommunityInvite: TypeConstructorDescription { + case exportedCommunityInvite(flags: Int32, title: String, url: String, peers: [Api.Peer]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedCommunityInvite(let flags, let title, let url, let peers): + if boxed { + buffer.appendInt32(-337788502) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedCommunityInvite(let flags, let title, let url, let peers): + return ("exportedCommunityInvite", [("flags", flags as Any), ("title", title as Any), ("url", url as Any), ("peers", peers as Any)]) + } + } + + public static func parse_exportedCommunityInvite(_ reader: BufferReader) -> ExportedCommunityInvite? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: [Api.Peer]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ExportedCommunityInvite.exportedCommunityInvite(flags: _1!, title: _2!, url: _3!, peers: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum ExportedContactToken: TypeConstructorDescription { case exportedContactToken(url: String, expires: Int32) @@ -1064,43 +1118,3 @@ public extension Api { } } -public extension Api { - enum ExportedMessageLink: TypeConstructorDescription { - case exportedMessageLink(link: String, html: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedMessageLink(let link, let html): - if boxed { - buffer.appendInt32(1571494644) - } - serializeString(link, buffer: buffer, boxed: false) - serializeString(html, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedMessageLink(let link, let html): - return ("exportedMessageLink", [("link", link as Any), ("html", html as Any)]) - } - } - - public static func parse_exportedMessageLink(_ reader: BufferReader) -> ExportedMessageLink? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.ExportedMessageLink.exportedMessageLink(link: _1!, html: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 928e7ad570..8b9623cebe 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -1,3 +1,43 @@ +public extension Api { + enum ExportedMessageLink: TypeConstructorDescription { + case exportedMessageLink(link: String, html: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedMessageLink(let link, let html): + if boxed { + buffer.appendInt32(1571494644) + } + serializeString(link, buffer: buffer, boxed: false) + serializeString(html, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedMessageLink(let link, let html): + return ("exportedMessageLink", [("link", link as Any), ("html", html as Any)]) + } + } + + public static func parse_exportedMessageLink(_ reader: BufferReader) -> ExportedMessageLink? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ExportedMessageLink.exportedMessageLink(link: _1!, html: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum FileHash: TypeConstructorDescription { case fileHash(offset: Int64, limit: Int32, hash: Buffer) diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index be9194440d..3751938295 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -532,6 +532,42 @@ public extension Api { } } +public extension Api { + enum InputCommunity: TypeConstructorDescription { + case inputCommunityDialogFilter(filterId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputCommunityDialogFilter(let filterId): + if boxed { + buffer.appendInt32(450955169) + } + serializeInt32(filterId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputCommunityDialogFilter(let filterId): + return ("inputCommunityDialogFilter", [("filterId", filterId as Any)]) + } + } + + public static func parse_inputCommunityDialogFilter(_ reader: BufferReader) -> InputCommunity? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputCommunity.inputCommunityDialogFilter(filterId: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputContact: TypeConstructorDescription { case inputPhoneContact(clientId: Int64, phone: String, firstName: String, lastName: String) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 5045fa2670..770093bab7 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -85,7 +85,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .joinedByRequest) case let .messageActionWebViewDataSentMe(text, _), let .messageActionWebViewDataSent(text): return TelegramMediaAction(action: .webViewData(text)) - case let .messageActionGiftPremium(currency, amount, months): + case let .messageActionGiftPremium(_, currency, amount, months, _, _): return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months)) case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId): return TelegramMediaAction(action: .topicCreated(title: title, iconColor: iconColor, iconFileId: iconEmojiId)) diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift index 4f54b333ae..568eb5fac4 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -978,7 +978,7 @@ func multipartFetch( continueInBackground: Bool = false, useMainConnection: Bool = false ) -> Signal { - if network.useExperimentalFeatures, let _ = resource as? TelegramCloudMediaResource { + if network.useExperimentalFeatures, let _ = resource as? TelegramCloudMediaResource, !(resource is SecretFileMediaResource) { return multipartFetchV2( postbox: postbox, network: network, diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index c682dffef1..6a9cc74562 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 156 + return 158 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 65765f33c6..8dd9b0a045 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -174,6 +174,7 @@ extension ChatListFilterIncludePeers { } public struct ChatListFilterData: Equatable, Hashable { + public var isShared: Bool public var categories: ChatListFilterPeerCategories public var excludeMuted: Bool public var excludeRead: Bool @@ -182,6 +183,7 @@ public struct ChatListFilterData: Equatable, Hashable { public var excludePeers: [PeerId] public init( + isShared: Bool, categories: ChatListFilterPeerCategories, excludeMuted: Bool, excludeRead: Bool, @@ -189,6 +191,7 @@ public struct ChatListFilterData: Equatable, Hashable { includePeers: ChatListFilterIncludePeers, excludePeers: [PeerId] ) { + self.isShared = isShared self.categories = categories self.excludeMuted = excludeMuted self.excludeRead = excludeRead @@ -246,6 +249,7 @@ public enum ChatListFilter: Codable, Equatable { let emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon") let data = ChatListFilterData( + isShared: try container.decodeIfPresent(Bool.self, forKey: "isShared") ?? false, categories: ChatListFilterPeerCategories(rawValue: try container.decode(Int32.self, forKey: "categories")), excludeMuted: (try container.decode(Int32.self, forKey: "excludeMuted")) != 0, excludeRead: (try container.decode(Int32.self, forKey: "excludeRead")) != 0, @@ -275,6 +279,7 @@ public enum ChatListFilter: Codable, Equatable { try container.encode(title, forKey: "title") try container.encodeIfPresent(emoticon, forKey: "emoticon") + try container.encode(data.isShared, forKey: "isShared") try container.encode(data.categories.rawValue, forKey: "categories") try container.encode((data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted") try container.encode((data.excludeRead ? 1 : 0) as Int32, forKey: "excludeRead") @@ -297,6 +302,7 @@ extension ChatListFilter { title: title, emoticon: emoticon, data: ChatListFilterData( + isShared: false, categories: ChatListFilterPeerCategories(apiFlags: flags), excludeMuted: (flags & (1 << 11)) != 0, excludeRead: (flags & (1 << 12)) != 0, @@ -338,6 +344,43 @@ extension ChatListFilter { } ) ) + case let .dialogFilterCommunity(_, id, title, emoticon, pinnedPeers, includePeers): + self = .filter( + id: id, + title: title, + emoticon: emoticon, + data: ChatListFilterData( + isShared: true, + categories: [], + excludeMuted: false, + excludeRead: false, + excludeArchived: false, + includePeers: ChatListFilterIncludePeers(rawPeers: includePeers.compactMap { peer -> PeerId? in + switch peer { + case let .inputPeerUser(userId, _): + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + return nil + } + }, rawPinnedPeers: pinnedPeers.compactMap { peer -> PeerId? in + switch peer { + case let .inputPeerUser(userId, _): + return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + return nil + } + }), + excludePeers: [] + ) + ) } } @@ -455,6 +498,46 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net } } + for peer in pinnedPeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + break + } + if let peerId = peerId, !missingChatIds.contains(peerId) { + if transaction.getPeerChatListIndex(peerId) == nil { + missingChatIds.insert(peerId) + missingChats.append(peer) + } + } + } + case let .dialogFilterCommunity(_, _, _, _, pinnedPeers, includePeers): + for peer in pinnedPeers + includePeers { + var peerId: PeerId? + switch peer { + case let .inputPeerUser(userId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + case let .inputPeerChat(chatId): + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)) + case let .inputPeerChannel(channelId, _): + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + default: + break + } + if let peerId = peerId { + if transaction.getPeer(peerId) == nil && !missingPeerIds.contains(peerId) { + missingPeerIds.insert(peerId) + missingPeers.append(peer) + } + } + } + for peer in pinnedPeers { var peerId: PeerId? switch peer { @@ -931,6 +1014,7 @@ public struct ChatListFeaturedFilter: Codable, Equatable { self.title = try container.decode(String.self, forKey: "title") self.description = try container.decode(String.self, forKey: "description") self.data = ChatListFilterData( + isShared: try container.decodeIfPresent(Bool.self, forKey: "isShared") ?? false, categories: ChatListFilterPeerCategories(rawValue: try container.decode(Int32.self, forKey: "categories")), excludeMuted: (try container.decode(Int32.self, forKey: "excludeMuted")) != 0, excludeRead: (try container.decode(Int32.self, forKey: "excludeRead")) != 0, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift new file mode 100644 index 0000000000..8a48d13169 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -0,0 +1,304 @@ +import Foundation +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 enum ExportChatFolderError { + case generic +} + +public struct ExportedChatFolderLink: Equatable { + public var title: String + public var link: String + public var peerIds: [EnginePeer.Id] + public var isRevoked: Bool + + public init( + title: String, + link: String, + peerIds: [EnginePeer.Id], + isRevoked: Bool + ) { + self.title = title + self.link = link + self.peerIds = peerIds + self.isRevoked = isRevoked + } +} + +public extension ExportedChatFolderLink { + var slug: String { + var slug = self.link + if slug.hasPrefix("https://t.me/folder/") { + slug = String(slug[slug.index(slug.startIndex, offsetBy: "https://t.me/folder/".count)...]) + } + return slug + } +} + +func _internal_exportChatFolder(account: Account, filterId: Int32, title: String, peerIds: [PeerId]) -> Signal { + return account.postbox.transaction { transaction -> [Api.InputPeer] in + return peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer) + } + |> 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 + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Signal in + switch result { + case let .exportedCommunityInvite(filter, invite): + let parsedFilter = ChatListFilter(apiFilter: filter) + + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + if let index = state.filters.firstIndex(where: { $0.id == filterId }) { + state.filters[index] = parsedFilter + } else { + state.filters.append(parsedFilter) + } + state.remoteFilters = state.filters + return state + }) + + switch invite { + case let .exportedCommunityInvite(flags, title, url, peers): + return .single(ExportedChatFolderLink( + title: title, + link: url, + peerIds: peers.map(\.peerId), + isRevoked: (flags & (1 << 0)) != 0 + )) + } + } + } + |> castError(ExportChatFolderError.self) + |> switchToLatest + } + } +} + +func _internal_getExportedChatFolderLinks(account: Account, id: Int32) -> Signal<[ExportedChatFolderLink], NoError> { + return account.network.request(Api.functions.communities.getExportedInvites(community: .inputCommunityDialogFilter(filterId: id))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[ExportedChatFolderLink], NoError> in + guard let result = result else { + return .single([]) + } + return account.postbox.transaction { transaction -> [ExportedChatFolderLink] in + switch result { + case let .exportedInvites(invites, chats, users): + var peers: [Peer] = [] + var peerPresences: [PeerId: Api.User] = [:] + + for user in users { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + peerPresences[telegramUser.id] = user + } + for chat in chats { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + peers.append(peer) + } + } + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + + var result: [ExportedChatFolderLink] = [] + for invite in invites { + switch invite { + case let .exportedCommunityInvite(flags, title, url, peers): + result.append(ExportedChatFolderLink( + title: title, + link: url, + peerIds: peers.map(\.peerId), + isRevoked: (flags & (1 << 0)) != 0 + )) + } + } + + return result + } + } + } +} + +public enum EditChatFolderLinkError { + case generic +} + +func _internal_editChatFolderLink(account: Account, filterId: Int32, link: ExportedChatFolderLink, title: String?, revoke: Bool) -> Signal { + var flags: Int32 = 0 + if revoke { + flags |= 1 << 0 + } + if title != nil { + flags |= 1 << 1 + } + return account.network.request(Api.functions.communities.editExportedInvite(flags: flags, community: .inputCommunityDialogFilter(filterId: filterId), slug: link.slug, title: title)) + |> mapError { _ -> EditChatFolderLinkError in + return .generic + } + |> ignoreValues + +} + +public enum RevokeChatFolderLinkError { + case generic +} + +func _internal_revokeChatFolderLink(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 + return .generic + } + |> ignoreValues +} + +public enum CheckChatFolderLinkError { + case generic +} + +public final class ChatFolderLinkContents { + public let localFilterId: Int32? + public let title: String? + public let peers: [EnginePeer] + public let alreadyMemberPeerIds: Set + + public init( + localFilterId: Int32?, + title: String?, + peers: [EnginePeer], + alreadyMemberPeerIds: Set + ) { + self.localFilterId = localFilterId + self.title = title + self.peers = peers + self.alreadyMemberPeerIds = alreadyMemberPeerIds + } +} + +func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal { + return account.network.request(Api.functions.communities.checkCommunityInvite(slug: slug)) + |> mapError { _ -> CheckChatFolderLinkError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> ChatFolderLinkContents in + switch result { + case let .communityInvite(title, peers, chats, users): + var allPeers: [Peer] = [] + var peerPresences: [PeerId: Api.User] = [:] + + for user in users { + let telegramUser = TelegramUser(user: user) + allPeers.append(telegramUser) + peerPresences[telegramUser.id] = user + } + for chat in chats { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + allPeers.append(peer) + } + } + + updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + + var resultPeers: [EnginePeer] = [] + var alreadyMemberPeerIds = Set() + for peer in peers { + if let peerValue = transaction.getPeer(peer.peerId) { + resultPeers.append(EnginePeer(peerValue)) + + if transaction.getPeerChatListIndex(peer.peerId) != nil { + alreadyMemberPeerIds.insert(peer.peerId) + } + } + } + + return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds) + case let .communityInviteAlready(filterId, missingPeers, chats, users): + var allPeers: [Peer] = [] + var peerPresences: [PeerId: Api.User] = [:] + + for user in users { + let telegramUser = TelegramUser(user: user) + allPeers.append(telegramUser) + peerPresences[telegramUser.id] = user + } + for chat in chats { + if let peer = parseTelegramGroupOrChannel(chat: chat) { + allPeers.append(peer) + } + } + + updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + + let currentFilters = _internal_currentChatListFilters(transaction: transaction) + var currentFilterTitle: String? + if let index = currentFilters.firstIndex(where: { $0.id == filterId }) { + switch currentFilters[index] { + case let .filter(_, title, _, _): + currentFilterTitle = title + default: + break + } + } + + var resultPeers: [EnginePeer] = [] + var alreadyMemberPeerIds = Set() + for peer in missingPeers { + if let peerValue = transaction.getPeer(peer.peerId) { + resultPeers.append(EnginePeer(peerValue)) + + if transaction.getPeerChatListIndex(peer.peerId) != nil { + alreadyMemberPeerIds.insert(peer.peerId) + } + } + } + + return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds) + } + } + |> castError(CheckChatFolderLinkError.self) + } +} + +public enum JoinChatFolderLinkError { + case generic +} + +func _internal_joinChatFolderLink(account: Account, slug: String, 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.joinCommunityInvite(slug: slug, peers: inputPeers)) + |> mapError { _ -> JoinChatFolderLinkError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return .complete() + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 963e3b98b3..75d5d0908a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1025,6 +1025,30 @@ public extension TelegramEngine { |> ignoreValues } } + + public func exportChatFolder(filterId: Int32, title: String, peerIds: [PeerId]) -> Signal { + return _internal_exportChatFolder(account: self.account, filterId: filterId, title: title, peerIds: peerIds) + } + + public func getExportedChatFolderLinks(id: Int32) -> Signal<[ExportedChatFolderLink], NoError> { + return _internal_getExportedChatFolderLinks(account: self.account, id: id) + } + + public func editChatFolderLink(filterId: Int32, link: ExportedChatFolderLink, title: String?, revoke: Bool) -> Signal { + return _internal_editChatFolderLink(account: self.account, filterId: filterId, link: link, title: title, revoke: revoke) + } + + public func revokeChatFolderLink(filterId: Int32, link: ExportedChatFolderLink) -> Signal { + return _internal_revokeChatFolderLink(account: self.account, filterId: filterId, link: link) + } + + public func checkChatFolderLink(slug: String) -> Signal { + return _internal_checkChatFolderLink(account: self.account, slug: slug) + } + + public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal { + return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds) + } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index fc92fcbbac..2125a4eca0 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -358,6 +358,7 @@ swift_library( "//submodules/DrawingUI:DrawingUI", "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", "//submodules/TelegramUI/Components/SendInviteLinkScreen", + "//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen", "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD new file mode 100644 index 0000000000..9965bd5061 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatFolderLinkPreviewScreen", + module_name = "ChatFolderLinkPreviewScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/AvatarNode", + "//submodules/CheckNode", + "//submodules/Markdown", + "//submodules/UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift new file mode 100644 index 0000000000..6055b622c4 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkHeaderComponent.swift @@ -0,0 +1,187 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import AccountContext +import MultilineTextComponent +import TelegramPresentationData + +final class ChatFolderLinkHeaderComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let title: String + let badge: String? + + init( + theme: PresentationTheme, + strings: PresentationStrings, + title: String, + badge: String? + ) { + self.theme = theme + self.strings = strings + self.title = title + self.badge = badge + } + + static func ==(lhs: ChatFolderLinkHeaderComponent, rhs: ChatFolderLinkHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.badge != rhs.badge { + return false + } + return true + } + + final class View: UIView { + private let leftView = UIImageView() + private let rightView = UIImageView() + private let title = ComponentView() + private let separatorLayer = SimpleLayer() + + private var component: ChatFolderLinkHeaderComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.leftView) + self.addSubview(self.rightView) + self.layer.addSublayer(self.separatorLayer) + + self.separatorLayer.cornerRadius = 2.0 + self.separatorLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ChatFolderLinkHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + + let height: CGFloat = 60.0 + let spacing: CGFloat = 16.0 + + if themeUpdated { + //TODO:localize + let leftString = NSAttributedString(string: "All Chats", font: Font.semibold(14.0), textColor: component.theme.list.freeTextColor) + let rightString = NSAttributedString(string: "Personal", font: Font.semibold(14.0), textColor: component.theme.list.freeTextColor) + + let leftStringBounds = leftString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let rightStringBounds = rightString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + + self.leftView.image = generateImage(leftStringBounds.size.integralFloor, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + + leftString.draw(in: leftStringBounds) + + var colors: [UIColor] = [] + var locations: [CGFloat] = [] + for i in 0 ... 8 { + let t: CGFloat = CGFloat(i) / CGFloat(8) + let a: CGFloat = t * t + colors.append(UIColor(white: 1.0, alpha: a)) + locations.append(t) + } + + if let image = generateGradientImage(size: CGSize(width: size.width * 0.8, height: 16.0), colors: colors, locations: locations, direction: .horizontal) { + image.draw(in: CGRect(origin: CGPoint(), size: CGSize(width: image.size.width, height: size.height)), blendMode: .destinationIn, alpha: 1.0) + } + + UIGraphicsPopContext() + }) + self.leftView.alpha = 0.5 + + self.rightView.image = generateImage(rightStringBounds.size.integralFloor, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + + rightString.draw(in: rightStringBounds) + + var colors: [UIColor] = [] + var locations: [CGFloat] = [] + for i in 0 ... 8 { + let t: CGFloat = CGFloat(i) / CGFloat(8) + let a: CGFloat = 1.0 - t * t + colors.append(UIColor(white: 1.0, alpha: a)) + locations.append(t) + } + + if let image = generateGradientImage(size: CGSize(width: size.width * 0.8, height: 16.0), colors: colors, locations: locations, direction: .horizontal) { + image.draw(in: CGRect(origin: CGPoint(x: size.width - image.size.width, y: 0.0), size: CGSize(width: image.size.width, height: size.height)), blendMode: .destinationIn, alpha: 1.0) + } + + UIGraphicsPopContext() + }) + self.rightView.alpha = 0.5 + + self.separatorLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor + } + + var contentWidth: CGFloat = 0.0 + if let leftImage = self.leftView.image { + contentWidth += leftImage.size.width + } + contentWidth += spacing + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + contentWidth += titleSize.width + + contentWidth += spacing + if let rightImage = self.rightView.image { + contentWidth += rightImage.size.width + } + + var contentOriginX: CGFloat = 0.0 + + if let leftImage = self.leftView.image { + transition.setFrame(view: self.leftView, frame: CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - leftImage.size.height) / 2.0) - 1.0), size: leftImage.size)) + contentOriginX += leftImage.size.width + } + contentOriginX += spacing + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + 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))) + } + contentOriginX += titleSize.width + contentOriginX += spacing + + if let rightImage = self.rightView.image { + transition.setFrame(view: self.rightView, frame: CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - rightImage.size.height) / 2.0) - 1.0), size: rightImage.size)) + contentOriginX += rightImage.size.width + } + + return CGSize(width: contentWidth, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift new file mode 100644 index 0000000000..7caf63e270 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -0,0 +1,721 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import Postbox +import SolidRoundedButtonComponent +import PresentationDataUtils +import Markdown +import UndoUI + +private final class ChatFolderLinkPreviewScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let slug: String + let linkContents: ChatFolderLinkContents? + + init( + context: AccountContext, + slug: String, + linkContents: ChatFolderLinkContents? + ) { + self.context = context + self.slug = slug + self.linkContents = linkContents + } + + static func ==(lhs: ChatFolderLinkPreviewScreenComponent, rhs: ChatFolderLinkPreviewScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.slug != rhs.slug { + return false + } + if lhs.linkContents !== rhs.linkContents { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let topIcon = ComponentView() + + private let title = ComponentView() + private let leftButton = ComponentView() + private let descriptionText = ComponentView() + private let actionButton = ComponentView() + + private let listHeaderText = ComponentView() + private let itemContainerView: UIView + private var items: [AnyHashable: ComponentView] = [:] + + private var selectedItems = Set() + + private let bottomOverscrollLimit: CGFloat + + private var ignoreScrolling: Bool = false + + private var component: ChatFolderLinkPreviewScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + private var topOffsetDistance: CGFloat? + + private var joinDisposable: Disposable? + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.itemContainerView = UIView() + self.itemContainerView.clipsToBounds = true + self.itemContainerView.layer.cornerRadius = 10.0 + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.addSubview(self.navigationBarContainer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.scrollContentView.addSubview(self.itemContainerView) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.joinDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { + return + } + + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + + if topOffset < topOffsetDistance { + targetContentOffset.pointee.y = scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: Transition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = 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 + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + } + + func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + + if self.component?.linkContents == nil, let linkContents = component.linkContents { + for peer in linkContents.peers { + self.selectedItems.insert(peer.id) + } + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor + self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + + let leftButtonSize = self.leftButton.update( + transition: transition, + 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 + guard let self, let controller = self.environment?.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: 44.0, height: 56.0))), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: leftButtonSize) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.navigationBarContainer.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + } + + let titleString: String + if let linkContents = component.linkContents { + //TODO:localize + if linkContents.localFilterId != nil { + if self.selectedItems.count == 1 { + titleString = "Add \(self.selectedItems.count) chat" + } else { + titleString = "Add \(self.selectedItems.count) chats" + } + } else { + titleString = "Add Folder" + } + } else { + titleString = " " + } + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 18.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + contentHeight += 44.0 + contentHeight += 14.0 + + let topIconSize = self.topIcon.update( + transition: transition, + component: AnyComponent(ChatFolderLinkHeaderComponent( + theme: environment.theme, + strings: environment.strings, + title: component.linkContents?.title ?? "Folder", + badge: nil + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) + ) + let topIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topIconSize.width) * 0.5), y: contentHeight), size: topIconSize) + if let topIconView = self.topIcon.view { + if topIconView.superview == nil { + self.scrollContentView.addSubview(topIconView) + } + transition.setFrame(view: topIconView, frame: topIconFrame) + topIconView.isHidden = component.linkContents == nil + } + + contentHeight += topIconSize.height + contentHeight += 20.0 + + let text: String + if let linkContents = component.linkContents { + if linkContents.localFilterId == nil { + text = "Do you want to add a new chat folder\nand join its groups and channels?" + } else { + let chatCountString: String + if self.selectedItems.count == 1 { + chatCountString = "1 chat" + } else { + chatCountString = "\(self.selectedItems.count) chats" + } + if let title = linkContents.title { + text = "Do you want to add **\(chatCountString)** to your\nfolder **\(title)**?" + } else { + text = "Do you want to add **\(chatCountString)** chats to your\nfolder?" + } + } + } else { + text = " " + } + + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor) + + let descriptionTextSize = self.descriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: text, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) + ) + let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) + if let descriptionTextView = self.descriptionText.view { + if descriptionTextView.superview == nil { + self.scrollContentView.addSubview(descriptionTextView) + } + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + } + + contentHeight += descriptionTextFrame.height + contentHeight += 39.0 + + var singleItemHeight: CGFloat = 0.0 + + var itemsHeight: CGFloat = 0.0 + var validIds: [AnyHashable] = [] + if let linkContents = component.linkContents { + for i in 0 ..< linkContents.peers.count { + let peer = linkContents.peers[i] + + for _ in 0 ..< 1 { + //let id: AnyHashable = AnyHashable("\(peer.id)_\(j)") + let id = AnyHashable(peer.id) + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item + } + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: 0.0, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + subtitle: nil, + 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 { + return + } + if self.selectedItems.contains(peer.id) { + self.selectedItems.remove(peer.id) + } else { + self.selectedItems.insert(peer.id) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height + } + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.items { + if !validIds.contains(id) { + removeIds.append(id) + item.view?.removeFromSuperview() + } + } + for id in removeIds { + self.items.removeValue(forKey: id) + } + + let listHeaderTitle: String + if self.selectedItems.count == 1 { + listHeaderTitle = "1 CHAT IN FOLDER TO JOIN" + } else { + listHeaderTitle = "\(self.selectedItems.count) CHATS IN FOLDER TO JOIN" + } + + let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor) + + let listHeaderTextSize = self.listHeaderText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: listHeaderTitle, + attributes: MarkdownAttributes( + body: listHeaderBody, + bold: listHeaderBody, + link: listHeaderBody, + linkAttribute: { _ in nil } + ) + ) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0) + ) + if let listHeaderTextView = self.listHeaderText.view { + if listHeaderTextView.superview == nil { + listHeaderTextView.layer.anchorPoint = CGPoint() + self.scrollContentView.addSubview(listHeaderTextView) + } + let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize) + transition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin) + listHeaderTextView.bounds = CGRect(origin: CGPoint(), size: listHeaderTextFrame.size) + listHeaderTextView.isHidden = component.linkContents == nil + } + contentHeight += listHeaderTextSize.height + contentHeight += 6.0 + + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) + + var initialContentHeight = contentHeight + initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5)) + + contentHeight += itemsHeight + contentHeight += 24.0 + initialContentHeight += 24.0 + + let actionButtonTitle: String + if let linkContents = component.linkContents { + if linkContents.localFilterId != nil { + actionButtonTitle = "Join Chats" + } else { + actionButtonTitle = "Add Folder" + } + } else { + actionButtonTitle = " " + } + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(SolidRoundedButtonComponent( + title: actionButtonTitle, + badge: (self.selectedItems.isEmpty) ? 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, + animationName: nil, + iconPosition: .right, + iconSpacing: 4.0, + isLoading: component.linkContents == nil, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + + if let _ = component.linkContents { + if self.joinDisposable == nil, !self.selectedItems.isEmpty { + self.joinDisposable = (component.context.engine.peers.joinChatFolderLink(slug: component.slug, peerIds: Array(self.selectedItems)) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + 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: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let bottomPanelHeight = 14.0 + environment.safeInsets.bottom + actionButtonSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + contentHeight += bottomPanelHeight + initialContentHeight += bottomPanelHeight + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 10.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset + 56.0))) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + 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) + } +} + +public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + private var linkContents: ChatFolderLinkContents? + private var linkContentsDisposable: Disposable? + + private var isDismissed: Bool = false + + public init(context: AccountContext, slug: String) { + self.context = context + + super.init(context: context, component: ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: nil), 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) + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.linkContentsDisposable?.dispose() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? ChatFolderLinkPreviewScreenComponent.View { + componentView.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? ChatFolderLinkPreviewScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift new file mode 100644 index 0000000000..eb72a2a56e --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift @@ -0,0 +1,327 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import MultilineTextComponent +import Postbox +import AvatarNode +import TelegramPresentationData +import CheckNode +import TelegramStringFormatting + +private let avatarFont = avatarPlaceholderFont(size: 15.0) + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } +} + +final class PeerListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool, isTinted: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let sideInset: CGFloat + let title: String + let peer: EnginePeer? + let subtitle: String? + let selectionState: SelectionState + let hasNext: Bool + let action: (EnginePeer) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + sideInset: CGFloat, + title: String, + peer: EnginePeer?, + subtitle: String?, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.sideInset = sideInset + self.title = title + self.peer = peer + self.subtitle = subtitle + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let avatarNode: AvatarNode + + private var checkLayer: CheckLayer? + + private var component: PeerListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + self.containerButton.layer.addSublayer(self.avatarNode.layer) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component, let peer = component.peer else { + return + } + component.action(peer) + } + + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 60.0 + let verticalInset: CGFloat = 1.0 + var leftInset: CGFloat = 62.0 + component.sideInset + let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + var avatarLeftInset: CGFloat = component.sideInset + 10.0 + + if case let .editing(isSelected, isTinted) = component.selectionState { + leftInset += 44.0 + avatarLeftInset += 44.0 + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + var theme = CheckNodeTheme(theme: component.theme, style: .plain) + if isTinted { + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35) + } + checkLayer.theme = theme + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + var theme = CheckNodeTheme(theme: component.theme, style: .plain) + if isTinted { + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35) + } + checkLayer = CheckLayer(theme: theme) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let avatarSize: CGFloat = 40.0 + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + if self.avatarNode.bounds.isEmpty { + self.avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) + } + if let peer = component.peer { + let clipStyle: AvatarNodeClipStyle + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + if peer.id == component.context.account.peerId { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } else { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + } + + //TODO:localize + let labelData: (String, Bool) + if let subtitle = component.subtitle { + labelData = (subtitle, false) + } else if case .legacyGroup = component.peer { + labelData = ("group", false) + } else if case let .channel(channel) = component.peer { + if case .group = channel.info { + labelData = ("group", false) + } else { + labelData = ("channel", false) + } + } else { + labelData = ("group", false) + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated && !"".isEmpty { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSpacing: CGFloat = 1.0 + let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.containerButton.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 1bdf8d5238..f3b45b9945 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -1021,6 +1021,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } else { strongSelf.controllerInteraction.presentController(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) } + case .chatFolder: + break case let .instantView(webpage, anchor): strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor)) case let .join(link): diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ae23ba00c4..12a02756dd 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -29,6 +29,7 @@ import WebUI import BotPaymentsUI import PremiumUI import AuthorizationUI +import ChatFolderLinkPreviewScreen private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -750,5 +751,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } else { present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } + case let .chatFolder(slug): + if let navigationController = navigationController { + navigationController.pushViewController(ChatFolderLinkPreviewScreen(context: context, slug: slug)) + } } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 1e3921eeb7..c388cc39cd 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -837,6 +837,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } handleResolvedUrl(.premiumOffer(reference: reference)) + } else if parsedUrl.host == "folder" { + if let components = URLComponents(string: "/?" + query) { + var slug: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "slug" { + slug = value + } + } + } + } + if let slug = slug { + convertedUrl = "https://t.me/folder/\(slug)" + } + } } } else { if parsedUrl.host == "importStickers" { diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index b4dd071d1e..099656200b 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -97,6 +97,7 @@ public enum ParsedInternalUrl { case phone(String, String?, String?) case startAttach(String, String?, String?) case contactToken(String) + case chatFolder(slug: String) } private enum ParsedUrl { @@ -417,6 +418,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .wallpaper(parameter) } else if pathComponents[0] == "addtheme" { return .theme(pathComponents[1]) + } else if 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]) { var threadId: Int32? @@ -774,6 +777,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } case let .stickerPack(name, type): return .single(.stickerPack(name: name, type: type)) + case let .chatFolder(slug): + return .single(.chatFolder(slug: slug)) case let .invoice(slug): return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) |> map(Optional.init)