diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 251c124056..d3d5e444c4 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -90,6 +90,7 @@ swift_library( "//submodules/AvatarVideoNode:AvatarVideoNode", "//submodules/InviteLinksUI", "//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen", + "//submodules/ItemListUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 4b48549191..802dd49174 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1547,8 +1547,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController for filter in filters { if filter.id == filterId, case let .filter(_, title, _, data) = filter { if !data.includePeers.peers.isEmpty { - items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) + items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { guard let strongSelf = self else { @@ -2701,6 +2701,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) { + openCreateChatListFolderLink(context: self.context, folderId: filterId, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in + self?.push(c) + }, presentController: { [weak self] c in + self?.present(c, in: .window(.root)) + }, linkUpdated: { _ in + }) + /*self.push(folderInviteLinkListController(context: self.context, filterId: filterId, title: title, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { _ in }))*/ } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 09a57770ce..cf8255ccb0 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -186,7 +186,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {}) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index c288034a71..3cd416c788 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -519,7 +519,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case .inviteLinkHeader: //TODO:localize return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section) - case let.inviteLinkCreate(hasLinks): + case let .inviteLinkCreate(hasLinks): //TODO:localize return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? "Create a New Link" : "Share Folder", sectionId: self.section, editing: false, action: { arguments.createLink() @@ -654,14 +654,21 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) } - if !isNewFilter, let inviteLinks { + if !isNewFilter { entries.append(.inviteLinkHeader) - entries.append(.inviteLinkCreate(hasLinks: !inviteLinks.isEmpty)) - var index = 0 - for link in inviteLinks { - entries.append(.inviteLink(index, link)) - index += 1 + var hasLinks = false + if let inviteLinks, !inviteLinks.isEmpty { + hasLinks = true + } + entries.append(.inviteLinkCreate(hasLinks: hasLinks)) + + if let inviteLinks { + var index = 0 + for link in inviteLinks { + entries.append(.inviteLink(index, link)) + index += 1 + } } entries.append(.inviteLinkInfo) @@ -1065,6 +1072,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var focusOnNameImpl: (() -> Void)? + var applyImpl: ((@escaping () -> Void) -> Void)? let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil) if let currentPreset { @@ -1273,114 +1281,77 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } }, createLink: { - let state = stateValue.with({ $0 }) - - if let currentPreset, !state.additionallyIncludePeers.isEmpty { - let _ = (context.engine.data.get( - EngineDataList(state.additionallyIncludePeers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) - ) - |> deliverOnMainQueue).start(next: { peers in - let peers = peers.compactMap({ $0 }) - if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { - pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: nil, linkUpdated: { updatedLink in - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - guard var links else { - return - } - - if let updatedLink { - links.insert(updatedLink, at: 0) - sharedLinks.set(.single(links)) - } - }) - })) - } else { - let _ = (context.engine.peers.exportChatFolder(filterId: currentPreset.id, title: "", peerIds: state.additionallyIncludePeers) - |> deliverOnMainQueue).start(next: { link in - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - guard var links else { - return - } - - links.insert(link, at: 0) - sharedLinks.set(.single(links)) - }) - - pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: link, linkUpdated: { updatedLink in - if updatedLink != link { - let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in - guard var links else { - return - } - - if let updatedLink { - if let index = links.firstIndex(where: { $0 == link }) { - links.remove(at: index) - } - links.insert(updatedLink, at: 0) - sharedLinks.set(.single(links)) - } else { - if let index = links.firstIndex(where: { $0 == link }) { - links.remove(at: index) - sharedLinks.set(.single(links)) - } - } - }) - } - })) - }, error: { error in - //TODO:localize - let text: String - switch error { - case .generic: - text = "An error occurred" - case let .limitExceeded(limit, premiumLimit): - if limit < premiumLimit { - let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: { - }) - pushControllerImpl?(limitController) - - return - } - text = "You can't create more links." - } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - }) - } - }) - } - }, openLink: { link in - if let currentPreset, let _ = currentPreset.data { + applyImpl?({ let state = stateValue.with({ $0 }) - pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: link, linkUpdated: { updatedLink in - if updatedLink != link { + + if let currentPreset, let data = currentPreset.data { + //TODO:localize + var unavailableText: String? + if !data.categories.isEmpty || data.excludeArchived || data.excludeRead || data.excludeMuted || !data.excludePeers.isEmpty { + unavailableText = "You can't share a link to this folder." + } + if let unavailableText { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + + return + } + + openCreateChatListFolderLink(context: context, folderId: currentPreset.id, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in + pushControllerImpl?(c) + }, presentController: { c in + presentControllerImpl?(c, nil) + }, linkUpdated: { updatedLink in 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) + if let index = links.firstIndex(where: { $0.link == updatedLink.link }) { + links[index] = updatedLink + } else { + links.insert(updatedLink, at: 0) } - 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)) - } } }) - } - })) + }) + } + }) + }, openLink: { link in + if let currentPreset, let _ = currentPreset.data { + applyImpl?({ + let state = stateValue.with({ $0 }) + pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: link, linkUpdated: { updatedLink in + if updatedLink != link { + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + guard var links else { + 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)) + } + } + }) + } + })) + }) } } ) var attemptNavigationImpl: (() -> Bool)? - let applyImpl: (() -> Void)? = { + applyImpl = { completed in let state = stateValue.with { $0 } let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var includePeers = ChatListFilterIncludePeers() @@ -1422,7 +1393,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } |> deliverOnMainQueue).start(next: { filters in updated(filters) - dismissImpl?() + completed() }) } @@ -1449,7 +1420,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } }) let rightNavigationButton = ItemListNavigationButton(content: .text(currentPreset == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { - applyImpl?() + applyImpl?({ + dismissImpl?() + }) }) let previousStateValue = previousState @@ -1531,3 +1504,42 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat return controller } +func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, title: String, peerIds: [EnginePeer.Id], pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) { + if peerIds.isEmpty { + return + } + let _ = (context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { peers in + let peers = peers.compactMap({ $0 }) + if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { + pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peerIds, currentInvitation: nil, linkUpdated: linkUpdated)) + } else { + let _ = (context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: peerIds) + |> deliverOnMainQueue).start(next: { link in + linkUpdated(link) + + pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: link.peerIds, currentInvitation: link, linkUpdated: linkUpdated)) + }, error: { error in + //TODO:localize + let text: String + switch error { + case .generic: + text = "An error occurred" + case let .limitExceeded(limit, premiumLimit): + if limit < premiumLimit { + let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: { + }) + pushController(limitController) + + return + } + text = "You can't create more links." + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + }) + } + }) +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4833ad97f4..e1354ef937 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2165,6 +2165,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openPasswordSetup: { }, openPremiumIntro: { }, openChatFolderUpdates: { + }, hideChatFolderUpdates: { }) chatListInteraction.isSearchMode = true @@ -3398,7 +3399,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }) var isInlineMode = false if case .topics = key { isInlineMode = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index c897c5e2b0..d40a0900a2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -97,6 +97,7 @@ public final class ChatListNodeInteraction { let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void let openChatFolderUpdates: () -> Void + let hideChatFolderUpdates: () -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -142,7 +143,8 @@ public final class ChatListNodeInteraction { openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, - openChatFolderUpdates: @escaping () -> Void + openChatFolderUpdates: @escaping () -> Void, + hideChatFolderUpdates: @escaping () -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -176,6 +178,7 @@ public final class ChatListNodeInteraction { self.openPasswordSetup = openPasswordSetup self.openPremiumIntro = openPremiumIntro self.openChatFolderUpdates = openChatFolderUpdates + self.hideChatFolderUpdates = hideChatFolderUpdates } } @@ -616,16 +619,26 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case let .ArchiveIntro(presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) case let .Notice(presentationData, notice): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] in - switch notice { - case .clearStorage: - nodeInteraction?.openStorageManagement() - case .setupPassword: - nodeInteraction?.openPasswordSetup() - case .premiumUpgrade, .premiumAnnualDiscount: - nodeInteraction?.openPremiumIntro() - case .chatFolderUpdates: - nodeInteraction?.openChatFolderUpdates() + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in + switch action { + case .activate: + switch notice { + case .clearStorage: + nodeInteraction?.openStorageManagement() + case .setupPassword: + nodeInteraction?.openPasswordSetup() + case .premiumUpgrade, .premiumAnnualDiscount: + nodeInteraction?.openPremiumIntro() + case .chatFolderUpdates: + nodeInteraction?.openChatFolderUpdates() + } + case .hide: + switch notice { + case .chatFolderUpdates: + nodeInteraction?.hideChatFolderUpdates() + default: + break + } } }), directionHint: entry.directionHint) } @@ -871,16 +884,26 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL case let .ArchiveIntro(presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) case let .Notice(presentationData, notice): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] in - switch notice { - case .clearStorage: - nodeInteraction?.openStorageManagement() - case .setupPassword: - nodeInteraction?.openPasswordSetup() - case .premiumUpgrade, .premiumAnnualDiscount: - nodeInteraction?.openPremiumIntro() - case .chatFolderUpdates: - nodeInteraction?.openChatFolderUpdates() + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in + switch action { + case .activate: + switch notice { + case .clearStorage: + nodeInteraction?.openStorageManagement() + case .setupPassword: + nodeInteraction?.openPasswordSetup() + case .premiumUpgrade, .premiumAnnualDiscount: + nodeInteraction?.openPremiumIntro() + case .chatFolderUpdates: + nodeInteraction?.openChatFolderUpdates() + } + case .hide: + switch notice { + case .chatFolderUpdates: + nodeInteraction?.hideChatFolderUpdates() + default: + break + } } }), directionHint: entry.directionHint) case .HeaderEntry: @@ -1076,8 +1099,9 @@ public final class ChatListNode: ListView { let hideArhiveIntro = ValuePromise(false, ignoreRepeated: true) - private let chatFolderUpdates = Promise(nil) + private let chatFolderUpdates = Promise() private var pollFilterUpdatesDisposable: Disposable? + private var chatFilterUpdatesDisposable: Disposable? public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) { self.context = context @@ -1413,6 +1437,21 @@ public final class ChatListNode: ListView { self.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(result), contents: result.chatFolderLinkContents)) }) + }, hideChatFolderUpdates: { [weak self] in + guard let self else { + return + } + let _ = (self.chatFolderUpdates.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let result else { + return + } + + if let localFilterId = result.chatFolderLinkContents.localFilterId { + let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start() + } + }) }) nodeInteraction.isInlineMode = isInlineMode @@ -1973,6 +2012,7 @@ public final class ChatListNode: ListView { var didIncludeRemovingPeerId = false var didIncludeHiddenByDefaultArchive = false var didIncludeHiddenThread = false + var didIncludeNotice = false if let previous = previousView { for entry in previous.filteredEntries { if case let .PeerEntry(peerEntry) = entry { @@ -1999,12 +2039,15 @@ public final class ChatListNode: ListView { } } else if case let .GroupReferenceEntry(_, _, _, _, _, _, _, _, hiddenByDefault) = entry { didIncludeHiddenByDefaultArchive = hiddenByDefault + } else if case .Notice = entry { + didIncludeNotice = true } } } var doesIncludeRemovingPeerId = false var doesIncludeArchive = false var doesIncludeHiddenByDefaultArchive = false + var doesIncludeNotice = false var doesIncludeHiddenThread = false for entry in processedView.filteredEntries { @@ -2033,6 +2076,8 @@ public final class ChatListNode: ListView { } else if case let .GroupReferenceEntry(_, _, _, _, _, _, _, _, hiddenByDefault) = entry { doesIncludeArchive = true doesIncludeHiddenByDefaultArchive = hiddenByDefault + } else if case .Notice = entry { + doesIncludeNotice = true } } if previousPinnedChats != updatedPinnedChats || previousPinnedThreads != updatedPinnedThreads { @@ -2059,6 +2104,9 @@ public final class ChatListNode: ListView { if didIncludeHiddenThread != doesIncludeHiddenThread { disableAnimations = false } + if didIncludeNotice != doesIncludeNotice { + disableAnimations = false + } } if let _ = previousHideArchivedFolderByDefaultValue, previousHideArchivedFolderByDefaultValue != hideArchivedFolderByDefault { @@ -2578,7 +2626,7 @@ public final class ChatListNode: ListView { } } - self.pollFilterUpdates(shouldDelay: false) + self.pollFilterUpdates() self.resetFilter() let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) @@ -2596,6 +2644,7 @@ public final class ChatListNode: ListView { self.activityStatusesDisposable?.dispose() self.updatedFilterDisposable.dispose() self.pollFilterUpdatesDisposable?.dispose() + self.chatFilterUpdatesDisposable?.dispose() } func updateFilter(_ filter: ChatListFilter?) { @@ -2605,17 +2654,18 @@ public final class ChatListNode: ListView { } } - private func pollFilterUpdates(shouldDelay: Bool) { + private func pollFilterUpdates() { guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else { + self.chatFolderUpdates.set(.single(nil)) return } - self.pollFilterUpdatesDisposable = (context.engine.peers.getChatFolderUpdates(folderId: id) - |> delay(shouldDelay ? 5.0 : 0.0, queue: .mainQueue())).start(next: { [weak self] result in + self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start() + self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id) + |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } self.chatFolderUpdates.set(.single(result)) - self.pollFilterUpdates(shouldDelay: true) }) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 98a51e5904..9b22eb782b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -7,16 +7,22 @@ import SwiftSignalKit import TelegramPresentationData import ListSectionHeaderNode import AppBundle +import ItemListUI class ChatListStorageInfoItem: ListViewItem { + enum Action { + case activate + case hide + } + let theme: PresentationTheme let strings: PresentationStrings let notice: ChatListNotice - let action: () -> Void + let action: (Action) -> Void let selectable: Bool = true - init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) { self.theme = theme self.strings = strings self.notice = notice @@ -26,7 +32,7 @@ class ChatListStorageInfoItem: ListViewItem { func selected(listView: ListView) { listView.clearHighlightAnimated(true) - self.action() + self.action(.activate) } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -72,7 +78,8 @@ private let separatorHeight = 1.0 / UIScreen.main.scale private let titleFont = Font.semibold(15.0) private let textFont = Font.regular(15.0) -class ChatListStorageInfoItemNode: ListViewItemNode { +class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { + private let contentContainer: ASDisplayNode private let titleNode: TextNode private let textNode: TextNode private let arrowNode: ASImageNode @@ -81,17 +88,23 @@ class ChatListStorageInfoItemNode: ListViewItemNode { private var item: ChatListStorageInfoItem? required init() { + self.contentContainer = ASDisplayNode() + self.titleNode = TextNode() self.textNode = TextNode() self.arrowNode = ASImageNode() self.separatorNode = ASDisplayNode() - super.init(layerBacked: false, dynamicBounce: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.clipsToBounds = true self.addSubnode(self.separatorNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - self.addSubnode(self.arrowNode) + self.contentContainer.addSubnode(self.titleNode) + self.contentContainer.addSubnode(self.textNode) + self.contentContainer.addSubnode(self.arrowNode) + + self.addSubnode(self.contentContainer) self.zPosition = 1.0 } @@ -201,8 +214,35 @@ class ChatListStorageInfoItemNode: ListViewItemNode { strongSelf.contentSize = layout.contentSize strongSelf.insets = layout.insets + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + + switch item.notice { + case .chatFolderUpdates: + //TODO:locallize + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: "Hide", icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + default: + strongSelf.setRevealOptions((left: [], right: [])) + } } }) } } + + override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + transition.updateSublayerTransformOffset(layer: self.contentContainer.layer, offset: CGPoint(x: offset, y: 0.0)) + } + + override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + if let item = self.item { + item.action(.hide) + } + + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index ee5260cb78..59b7b962c4 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -75,13 +75,20 @@ public enum ContextMenuActionBadgeColor { case inactive } -public struct ContextMenuActionBadge { +public struct ContextMenuActionBadge: Equatable { + public enum Style { + case badge + case label + } + public var value: String public var color: ContextMenuActionBadgeColor + public var style: Style - public init(value: String, color: ContextMenuActionBadgeColor) { + public init(value: String, color: ContextMenuActionBadgeColor, style: Style = .badge) { self.value = value self.color = color + self.style = style } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index a5d75d0db4..dba9b76e24 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -64,8 +64,11 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin private let titleLabelNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode private let iconNode: ASImageNode + private var badgeIconNode: ASImageNode? private var animationNode: AnimationNode? + private var currentBadge: (badge: ContextMenuActionBadge, image: UIImage)? + private var iconDisposable: Disposable? init( @@ -302,14 +305,66 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin iconSize = iconImage?.size } + let badgeSize: CGSize? + if let badge = self.item.badge { + var badgeImage: UIImage? + if let currentBadge = self.currentBadge, currentBadge.badge == badge { + badgeImage = currentBadge.image + } else { + let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor + let badgeString = NSAttributedString(string: badge.value, font: Font.semibold(11.0), textColor: badgeTextColor) + let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + let badgeSideInset: CGFloat = 3.0 + let badgeVerticalInset: CGFloat = 1.0 + let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height)) + badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath) + context.fillPath() + + UIGraphicsPushContext(context) + + badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + badgeSideInset + UIScreenPixel, y: badgeTextBounds.minY + badgeVerticalInset + UIScreenPixel)) + + UIGraphicsPopContext() + }) + } + + let badgeIconNode: ASImageNode + if let current = self.badgeIconNode { + badgeIconNode = current + } else { + badgeIconNode = ASImageNode() + self.badgeIconNode = badgeIconNode + self.addSubnode(badgeIconNode) + } + badgeIconNode.image = badgeImage + + badgeSize = badgeImage?.size + } else { + if let badgeIconNode = self.badgeIconNode { + self.badgeIconNode = nil + badgeIconNode.removeFromSupernode() + } + badgeSize = nil + } + var maxTextWidth: CGFloat = constrainedSize.width maxTextWidth -= sideInset + if let iconSize = iconSize { maxTextWidth -= max(standardIconWidth, iconSize.width) maxTextWidth -= iconSpacing } else { maxTextWidth -= sideInset } + + if let badgeSize = badgeSize { + maxTextWidth -= badgeSize.width + maxTextWidth -= 8.0 + } + maxTextWidth = max(1.0, maxTextWidth) let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0)) @@ -351,6 +406,12 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame) transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame) + if let badgeIconNode = self.badgeIconNode { + if let iconSize = badgeIconNode.image?.size { + transition.updateFrame(node: badgeIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 8.0, y: titleFrame.minY + floor((titleFrame.height - iconSize.height) * 0.5)), size: iconSize)) + } + } + if let iconSize = iconSize { let iconWidth = max(standardIconWidth, iconSize.width) let iconFrame = CGRect( diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index cdc6081021..c24a292438 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -95,6 +95,7 @@ public final class HashtagSearchController: TelegramBaseController { }, openPasswordSetup: { }, openPremiumIntro: { }, openChatFolderUpdates: { + }, hideChatFolderUpdates: { }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 91be1ea6d4..bad7273a0f 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -118,6 +118,7 @@ private class PremiumLimitAnimationComponent: Component { self.activeContainer.masksToBounds = true self.activeBackground = SimpleLayer() + self.activeBackground.anchorPoint = CGPoint() self.badgeView = UIView() self.badgeView.alpha = 0.0 @@ -244,13 +245,16 @@ private class PremiumLimitAnimationComponent: Component { let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight)) self.container.frame = containerFrame + let activityPosition: CGFloat = floor(containerFrame.width * component.badgePosition) + let activeWidth: CGFloat = containerFrame.width - activityPosition + if !component.isPremiumDisabled { - self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: containerFrame.width / 2.0, height: lineHeight)) - self.activeContainer.frame = CGRect(origin: CGPoint(x: containerFrame.width / 2.0, y: 0.0), size: CGSize(width: containerFrame.width / 2.0, height: lineHeight)) + self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)) + self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)) - self.activeBackground.bounds = CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 3.0 / 2.0, height: lineHeight)) + self.activeBackground.frame = CGRect(origin: .zero, size: CGSize(width: activeWidth * (1.0 + 0.35), height: lineHeight)) if self.activeBackground.animation(forKey: "movement") == nil { - self.activeBackground.position = CGPoint(x: containerFrame.width * 3.0 / 4.0 - self.activeBackground.frame.width * 0.35, y: lineHeight / 2.0) + self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) } } @@ -306,7 +310,7 @@ private class PremiumLimitAnimationComponent: Component { if let _ = self.badgeView.layer.animation(forKey: "appearance1") { } else { - self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * badgePosition, y: 82.0) + self.badgeView.center = CGPoint(x: availableSize.width * badgePosition, y: 82.0) } if self.badgeView.frame.maxX > availableSize.width { @@ -375,14 +379,16 @@ private class PremiumLimitAnimationComponent: Component { } self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) - let lineOffset = (self.activeBackground.frame.width - self.activeContainer.bounds.width) / 2.0 + let lineOffset = 0.0 let linePreviousValue = self.activeBackground.position.x var lineNewValue: CGFloat = lineOffset - if lineOffset - linePreviousValue < self.activeBackground.frame.width * 0.25 { - lineNewValue -= self.activeBackground.frame.width * 0.35 + if linePreviousValue < 0.0 { + lineNewValue = 0.0 + } else { + lineNewValue = -self.activeContainer.bounds.width * 0.35 } - self.activeBackground.position = CGPoint(x: lineNewValue, y: self.activeBackground.bounds.size.height / 2.0) - + self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0) + let badgeAnimation = CABasicAnimation(keyPath: "position.x") badgeAnimation.duration = 4.5 badgeAnimation.fromValue = badgePreviousValue @@ -585,16 +591,25 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { transition: context.transition ) + let activityPosition = floor(context.availableSize.width * component.badgePosition) + + var inactiveValueOpacity: CGFloat = 1.0 + if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 { + inactiveValueOpacity = 0.0 + } + context.add(inactiveTitle .position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) + .opacity(inactiveValueOpacity) ) context.add(inactiveValue - .position(CGPoint(x: context.availableSize.width / 2.0 - inactiveValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0)) + .position(CGPoint(x: activityPosition - inactiveValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0)) + .opacity(inactiveValueOpacity) ) context.add(activeTitle - .position(CGPoint(x: context.availableSize.width / 2.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) + .position(CGPoint(x: activityPosition + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) ) context.add(activeValue @@ -766,22 +781,26 @@ private final class LimitSheetContent: CombinedComponent { string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string } case .linksPerSharedFolder: - //TODO:localize - let limit = state.limits.maxSharedFolderInviteLinks - let premiumLimit = state.premiumLimits.maxSharedFolderInviteLinks + /*let count: Int32 = 5 + Int32("".count)// component.count + let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks + let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/ + + let count: Int32 = component.count + let limit: Int32 = state.limits.maxSharedFolderInviteLinks + let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks + iconName = "Premium/Link" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeText = "\(count)" + string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string + defaultValue = count > limit ? "\(limit)" : "" + premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) if isPremiumDisabled { badgeText = "\(limit)" string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string } case .membershipInSharedFolders: - //TODO:localize let limit = state.limits.maxSharedFolderJoin let premiumLimit = state.premiumLimits.maxSharedFolderJoin iconName = "Premium/Folder" diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index dcf605ce57..24d923b163 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -47,7 +47,7 @@ public final class QrCodeScreen: ViewController { case let .invite(invite, _): return invite.link ?? "" case let .chatFolder(slug): - return "https://t.me/folder/\(slug)" + return "https://t.me/list/\(slug)" } } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 89a298c377..7bbfd06c43 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index c979c575d3..3c15313e63 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -843,7 +843,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) func makeChatListItem( diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 2bd110fa32..55892a0071 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -367,7 +367,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}) + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }) func makeChatListItem( peer: EnginePeer, diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 06768f64d7..056930ae34 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -997,7 +997,7 @@ 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[-557919187] = { return Api.communities.CommunityInvite.parse_communityInvite($0) } + dict[59080097] = { return Api.communities.CommunityInvite.parse_communityInvite($0) } dict[-951718393] = { return Api.communities.CommunityInvite.parse_communityInviteAlready($0) } dict[-414818125] = { return Api.communities.CommunityUpdates.parse_communityUpdates($0) } dict[1805101290] = { return Api.communities.ExportedCommunityInvite.parse_exportedCommunityInvite($0) } diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 3327f46126..45727fa53f 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -896,16 +896,18 @@ public extension Api.channels { } public extension Api.communities { enum CommunityInvite: TypeConstructorDescription { - case communityInvite(title: String, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) + case communityInvite(flags: Int32, title: String, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) case communityInviteAlready(filterId: Int32, missingPeers: [Api.Peer], alreadyPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .communityInvite(let title, let peers, let chats, let users): + case .communityInvite(let flags, let title, let emoticon, let peers, let chats, let users): if boxed { - buffer.appendInt32(-557919187) + buffer.appendInt32(59080097) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(peers.count)) for item in peers { @@ -953,34 +955,40 @@ public extension Api.communities { 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 .communityInvite(let flags, let title, let emoticon, let peers, let chats, let users): + return ("communityInvite", [("flags", flags as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) case .communityInviteAlready(let filterId, let missingPeers, let alreadyPeers, let chats, let users): return ("communityInviteAlready", [("filterId", filterId as Any), ("missingPeers", missingPeers as Any), ("alreadyPeers", alreadyPeers as Any), ("chats", chats as Any), ("users", users as Any)]) } } public static func parse_communityInvite(_ reader: BufferReader) -> CommunityInvite? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.Peer]? + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: [Api.Peer]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } - var _3: [Api.Chat]? + var _5: [Api.Chat]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _4: [Api.User]? + var _6: [Api.User]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.communities.CommunityInvite.communityInvite(title: _1!, peers: _2!, chats: _3!, users: _4!) + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.communities.CommunityInvite.communityInvite(flags: _1!, title: _2!, emoticon: _3, peers: _4!, chats: _5!, users: _6!) } else { return nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 2094dc938f..b93e375cbf 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -256,6 +256,7 @@ private enum PreferencesKeyValues: Int32 { case globalMessageAutoremoveTimeoutSettings = 27 case accountSpecificCacheStorageSettings = 28 case linksConfiguration = 29 + case chatListFilterUpdates = 30 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -402,6 +403,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.linksConfiguration.rawValue) return key }() + + public static let chatListFilterUpdates: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.chatListFilterUpdates.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 8dd9b0a045..62ec0338ff 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -872,14 +872,29 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, } struct ChatListFiltersState: Codable, Equatable { + struct ChatListFilterUpdates: Codable, Equatable { + var folderId: Int32 + var timestamp: Int32 + var peerIds: [PeerId] + + init(folderId: Int32, timestamp: Int32, peerIds: [PeerId]) { + self.folderId = folderId + self.timestamp = timestamp + self.peerIds = peerIds + } + } + var filters: [ChatListFilter] var remoteFilters: [ChatListFilter]? - static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil) + var updates: [ChatListFilterUpdates] - fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?) { + static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil, updates: []) + + fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?, updates: [ChatListFilterUpdates]) { self.filters = filters self.remoteFilters = remoteFilters + self.updates = updates } public init(from decoder: Decoder) throws { @@ -887,6 +902,7 @@ struct ChatListFiltersState: Codable, Equatable { self.filters = try container.decode([ChatListFilter].self, forKey: "filters") self.remoteFilters = try container.decodeIfPresent([ChatListFilter].self, forKey: "remoteFilters") + self.updates = try container.decodeIfPresent([ChatListFilterUpdates].self, forKey: "updates") ?? [] } func encode(to encoder: Encoder) throws { @@ -894,6 +910,14 @@ struct ChatListFiltersState: Codable, Equatable { try container.encode(self.filters, forKey: "filters") try container.encodeIfPresent(self.remoteFilters, forKey: "remoteFilters") + try container.encode(self.updates, forKey: "updates") + } + + mutating func normalize() { + if self.updates.isEmpty { + return + } + self.updates.removeAll(where: { update in !self.filters.contains(where: { $0.id == update.folderId }) }) } } @@ -918,6 +942,9 @@ func _internal_updateChatListFiltersInteractively(postbox: Postbox, _ f: @escapi hasUpdates = true } updated = updatedFilters + + state.normalize() + return PreferencesEntry(state) }) if hasUpdates { @@ -936,6 +963,7 @@ func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f: state.filters = updatedFilters hasUpdates = true } + state.normalize() return PreferencesEntry(state) }) if hasUpdates { @@ -943,7 +971,6 @@ func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f: } } - func _internal_updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> { return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) |> map { preferences -> [ChatListFilter] in @@ -953,6 +980,15 @@ func _internal_updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilte |> distinctUntilChanged } +func _internal_updatedChatListFiltersState(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) + |> map { preferences -> ChatListFiltersState in + let filtersState = preferences.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default + return filtersState + } + |> distinctUntilChanged +} + func _internal_updatedChatListFiltersInfo(postbox: Postbox) -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters]) |> map { preferences -> (filters: [ChatListFilter], synchronized: Bool) in @@ -982,11 +1018,17 @@ func _internal_currentChatListFilters(transaction: Transaction) -> [ChatListFilt return settings.filters } +func _internal_currentChatListFiltersState(transaction: Transaction) -> ChatListFiltersState { + let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters)?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default + return settings +} + func updateChatListFiltersState(transaction: Transaction, _ f: (ChatListFiltersState) -> ChatListFiltersState) -> ChatListFiltersState { var result: ChatListFiltersState? transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in let settings = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default - let updated = f(settings) + var updated = f(settings) + updated.normalize() result = updated return PreferencesEntry(updated) }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index cf31dece8b..1a6c95a077 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -60,7 +60,7 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String |> mapToSignal { inputPeers -> Signal in return account.network.request(Api.functions.communities.exportCommunityInvite(community: .inputCommunityDialogFilter(filterId: filterId), title: title, peers: inputPeers)) |> `catch` { error -> Signal in - if error.errorDescription == "INVITES_TOO_MUCH" { + if error.errorDescription == "INVITES_TOO_MUCH" || error.errorDescription == "FILTERS_TOO_MUCH" { return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false) } @@ -69,10 +69,18 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false) let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true) - if isPremium { - return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks)) + if error.errorDescription == "FILTERS_TOO_MUCH" { + if isPremium { + return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } else { + return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin)) + } } else { - return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks)) + if isPremium { + return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks)) + } else { + return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks)) + } } } } else { @@ -246,7 +254,9 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ChatFolderLinkContents in switch result { - case let .communityInvite(title, peers, chats, users): + case let .communityInvite(_, title, emoticon, peers, chats, users): + let _ = emoticon + var allPeers: [Peer] = [] var peerPresences: [PeerId: Api.User] = [:] @@ -321,7 +331,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Bool { if lhs.folderId != rhs.folderId { return false } - if lhs.missingPeers.map(\.peerId) != rhs.missingPeers.map(\.peerId) { + if lhs.missingPeers.map(\.id) != rhs.missingPeers.map(\.id) { return false } return true } } -func _internal_getChatFolderUpdates(account: Account, folderId: Int32) -> Signal { - return account.network.request(Api.functions.communities.getCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId))) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) +func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> Signal { + return account.postbox.transaction { transaction -> ChatListFiltersState in + return _internal_currentChatListFiltersState(transaction: transaction) } - |> mapToSignal { result -> Signal in - guard let result = result else { + |> mapToSignal { state -> Signal in + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let current = state.updates.first(where: { $0.folderId == folderId }) { + let updateInterval: Int32 + #if DEBUG + updateInterval = 5 + #else + updateInterval = 60 * 60 + #endif + + if current.timestamp + updateInterval >= timestamp { + return .complete() + } + } + + return account.network.request(Api.functions.communities.getCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId))) + |> map(Optional.init) + |> `catch` { _ -> Signal in return .single(nil) } - switch result { - case let .communityUpdates(missingPeers, chats, users): - return account.postbox.transaction { transaction -> ChatFolderUpdates? in - for filter in _internal_currentChatListFilters(transaction: transaction) { - if case let .filter(id, title, _, _) = filter, id == folderId { - return ChatFolderUpdates(folderId: folderId, title: title, missingPeers: missingPeers, chats: chats, users: users) - } + |> mapToSignal { result -> Signal in + guard let result = result else { + return account.postbox.transaction { transaction -> Void in + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + + state.updates.removeAll(where: { $0.folderId == folderId }) + state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: [])) + + return state + }) } - return nil + |> ignoreValues } + switch result { + case let .communityUpdates(missingPeers, chats, users): + return account.postbox.transaction { transaction -> Void in + 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) + + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + + state.updates.removeAll(where: { $0.folderId == folderId }) + state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId))) + + return state + }) + } + |> ignoreValues + } + } + } +} + +func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> Signal { + struct InternalData: Equatable { + var title: String + var peerIds: [EnginePeer.Id] + } + + return _internal_updatedChatListFiltersState(postbox: account.postbox) + |> map { state -> InternalData? in + guard let update = state.updates.first(where: { $0.folderId == folderId }) else { + return nil + } + guard let folder = state.filters.first(where: { $0.id == folderId }) else { + return nil + } + guard case let .filter(_, title, _, data) = folder, data.isShared else { + return nil + } + let filteredPeerIds: [PeerId] = update.peerIds.filter { !data.includePeers.peers.contains($0) } + return InternalData(title: title, peerIds: filteredPeerIds) + } + |> distinctUntilChanged + |> mapToSignal { internalData -> Signal in + guard let internalData = internalData else { + return .single(nil) + } + if internalData.peerIds.isEmpty { + return .single(nil) + } + return account.postbox.transaction { transaction -> ChatFolderUpdates? in + var peers: [EnginePeer] = [] + for peerId in internalData.peerIds { + if let peer = transaction.getPeer(peerId) { + peers.append(EnginePeer(peer)) + } + } + return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers) } } } @@ -530,11 +614,22 @@ func _internal_joinAvailableChatsInFolder(account: Account, updates: ChatFolderU } func _internal_hideChatFolderUpdates(account: Account, folderId: Int32) -> Signal { - return account.network.request(Api.functions.communities.hideCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId))) - |> `catch` { _ -> Signal in - return .single(.boolFalse) + return account.postbox.transaction { transaction -> Void in + let _ = updateChatListFiltersState(transaction: transaction, { state in + var state = state + + state.updates.removeAll(where: { $0.folderId == folderId }) + + return state + }) + } + |> mapToSignal { _ -> Signal in + return account.network.request(Api.functions.communities.hideCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId))) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues } - |> ignoreValues } func _internal_leaveChatFolder(account: Account, folderId: Int32, removePeerIds: [EnginePeer.Id]) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 798484b588..745302d881 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1050,8 +1050,20 @@ public extension TelegramEngine { return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds) } - public func getChatFolderUpdates(folderId: Int32) -> Signal { - return _internal_getChatFolderUpdates(account: self.account, folderId: folderId) + public func pollChatFolderUpdates(folderId: Int32) -> Signal { + let signal = _internal_pollChatFolderUpdatesOnce(account: self.account, folderId: folderId) + return ( + signal + |> then( + Signal.complete() + |> delay(10.0, queue: .concurrentDefaultQueue()) + ) + ) + |> restart + } + + public func subscribedChatFolderUpdates(folderId: Int32) -> Signal { + return _internal_subscribedChatFolderUpdates(account: self.account, folderId: folderId) } public func joinAvailableChatsInFolder(updates: ChatFolderUpdates, peerIds: [EnginePeer.Id]) -> Signal { diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 5ffd7a2460..d280a7adfd 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -370,7 +370,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentHeight += 14.0 var topBadge: String? - if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil { + if case .remove = component.subject { + } else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil { topBadge = "+\(linkContents.peers.count)" } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 9b4dc89e06..469c594ba0 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -266,6 +266,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, openPasswordSetup: { }, openPremiumIntro: { }, openChatFolderUpdates: { + }, hideChatFolderUpdates: { }) interaction.searchTextHighightState = searchQuery self.interaction = interaction