diff --git a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift new file mode 100644 index 0000000000..6f4e122c3d --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift @@ -0,0 +1,454 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import AccountContext +import TelegramPresentationData +import SwiftSignalKit +import AnimationCache +import MultiAnimationRenderer +import TelegramCore +import Postbox +import ChatListHeaderComponent +import ActionPanelComponent +import ChatFolderLinkPreviewScreen + +final class ChatListContainerItemNode: ASDisplayNode { + private final class TopPanelItem { + let view = ComponentView() + var size: CGSize? + + init() { + } + } + + private let context: AccountContext + private weak var controller: ChatListControllerImpl? + private let location: ChatListControllerLocation + private let animationCache: AnimationCache + private let animationRenderer: MultiAnimationRenderer + private var presentationData: PresentationData + private let becameEmpty: (ChatListFilter?) -> Void + private let emptyAction: (ChatListFilter?) -> Void + private let secondaryEmptyAction: () -> Void + private let openArchiveSettings: () -> Void + private let isInlineMode: Bool + + private var floatingHeaderOffset: CGFloat? + + private(set) var emptyNode: ChatListEmptyNode? + var emptyShimmerEffectNode: ChatListShimmerNode? + private var shimmerNodeOffset: CGFloat = 0.0 + let listNode: ChatListNode + + private var topPanel: TopPanelItem? + + private var pollFilterUpdatesDisposable: Disposable? + private var chatFilterUpdatesDisposable: Disposable? + private var peerDataDisposable: Disposable? + + private var chatFolderUpdates: ChatFolderUpdates? + + private var canReportPeer: Bool = false + + private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)? + private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)? + + init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) { + self.context = context + self.controller = controller + self.location = location + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.presentationData = presentationData + self.becameEmpty = becameEmpty + self.emptyAction = emptyAction + self.secondaryEmptyAction = secondaryEmptyAction + self.openArchiveSettings = openArchiveSettings + self.isInlineMode = isInlineMode + + self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady) + + if let controller, case .chatList(groupId: .root) = controller.location { + self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight + } + + super.init() + + self.addSubnode(self.listNode) + + self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in + guard let strongSelf = self else { + return + } + var needsShimmerNode = false + var shimmerNodeOffset: CGFloat = 0.0 + + var needsEmptyNode = false + var hasOnlyArchive = false + var hasOnlyGeneralThread = false + var isLoading = false + + switch isEmptyState { + case let .empty(isLoadingValue, hasArchiveInfo): + if hasArchiveInfo { + shimmerNodeOffset = 253.0 + } + if isLoadingValue { + needsShimmerNode = true + needsEmptyNode = false + isLoading = isLoadingValue + } else { + needsEmptyNode = true + } + if !isLoadingValue { + strongSelf.becameEmpty(filter) + } + case let .notEmpty(_, onlyHasArchiveValue, onlyGeneralThreadValue): + needsEmptyNode = onlyHasArchiveValue || onlyGeneralThreadValue + hasOnlyArchive = onlyHasArchiveValue + hasOnlyGeneralThread = onlyGeneralThreadValue + } + + if needsEmptyNode { + if let currentNode = strongSelf.emptyNode { + currentNode.updateIsLoading(isLoading) + } else { + let subject: ChatListEmptyNode.Subject + if let filter = filter { + var showEdit = true + if case let .filter(_, _, _, data) = filter { + if data.excludeRead && data.includePeers.peers.isEmpty && data.includePeers.pinnedPeers.isEmpty { + showEdit = false + } + } + subject = .filter(showEdit: showEdit) + } else { + if case .forum = location { + subject = .forum(hasGeneral: hasOnlyGeneralThread) + } else { + if case .chatList(groupId: .archive) = location { + subject = .archive + } else { + subject = .chats(hasArchive: hasOnlyArchive) + } + } + } + + let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: { + self?.emptyAction(filter) + }, secondaryAction: { + self?.secondaryEmptyAction() + }, openArchiveSettings: { + self?.openArchiveSettings() + }) + strongSelf.emptyNode = emptyNode + strongSelf.listNode.addSubnode(emptyNode) + if let (size, insets, _, _, _, _, _) = strongSelf.validLayout { + let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + emptyNode.frame = emptyNodeFrame + emptyNode.updateLayout(size: size, insets: insets, transition: .immediate) + + if let scrollingOffset = strongSelf.scrollingOffset { + emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate) + } + } + emptyNode.alpha = 0.0 + transition.updateAlpha(node: emptyNode, alpha: 1.0) + } + } else if let emptyNode = strongSelf.emptyNode { + strongSelf.emptyNode = nil + transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in + emptyNode?.removeFromSupernode() + }) + } + + + if needsShimmerNode { + strongSelf.shimmerNodeOffset = shimmerNodeOffset + if strongSelf.emptyShimmerEffectNode == nil { + let emptyShimmerEffectNode = ChatListShimmerNode() + strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode + strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode) + if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset { + strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate) + } + } + } else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode { + strongSelf.emptyShimmerEffectNode = nil + let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut) + emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in + emptyShimmerEffectNode?.removeFromSupernode() + }) + strongSelf.listNode.alpha = 0.0 + emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0) + } + } + + self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in + guard let strongSelf = self else { + return + } + strongSelf.floatingHeaderOffset = offset + if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode { + strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition) + } + strongSelf.layoutAdditionalPanels(transition: transition) + } + + if let filter, case let .filter(id, _, _, data) = filter, data.isShared { + 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 + } + var update = false + if let result, result.availableChatsToJoin != 0 { + if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin { + update = true + } + self.chatFolderUpdates = result + } else { + if self.chatFolderUpdates != nil { + self.chatFolderUpdates = nil + update = true + } + } + if update { + if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { + self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }) + } + + if case let .forum(peerId) = location { + self.peerDataDisposable = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] statusSettings in + guard let self else { + return + } + var canReportPeer = false + if let statusSettings, statusSettings.flags.contains(.canReport) { + canReportPeer = true + } + if self.canReportPeer != canReportPeer { + self.canReportPeer = canReportPeer + if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { + self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }) + } + } + + deinit { + self.pollFilterUpdatesDisposable?.dispose() + self.chatFilterUpdatesDisposable?.dispose() + self.peerDataDisposable?.dispose() + } + + private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) { + node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, isInlineMode: self.isInlineMode, presentationData: self.presentationData, transition: .immediate) + transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size)) + } + + private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) { + guard let (size, insets, visualNavigationHeight, _, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else { + return + } + + let _ = size + let _ = insets + + if let topPanel = self.topPanel, let topPanelSize = topPanel.size { + let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height + + if let topPanelView = topPanel.view.view { + var animateIn = false + var topPanelTransition = transition + if topPanelView.bounds.isEmpty { + topPanelTransition = .immediate + animateIn = true + } + topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize)) + if animateIn { + transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height)) + } + } + } + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + + self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) + + var listInsets = insets + var additionalTopInset: CGFloat = 0.0 + + if let chatFolderUpdates = self.chatFolderUpdates { + let topPanel: TopPanelItem + var topPanelTransition = Transition(transition) + if let current = self.topPanel { + topPanel = current + } else { + topPanelTransition = .immediate + topPanel = TopPanelItem() + self.topPanel = topPanel + } + + let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin)) + + let topPanelHeight: CGFloat = 44.0 + + let _ = topPanel.view.update( + transition: topPanelTransition, + component: AnyComponent(ActionPanelComponent( + theme: self.presentationData.theme, + title: title, + color: .accent, + action: { [weak self] in + guard let self, let chatFolderUpdates = self.chatFolderUpdates else { + return + } + + self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents)) + }, + dismissAction: { [weak self] in + guard let self, let chatFolderUpdates = self.chatFolderUpdates else { + return + } + let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start() + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: topPanelHeight) + ) + if let topPanelView = topPanel.view.view { + if topPanelView.superview == nil { + self.view.addSubview(topPanelView) + } + } + + topPanel.size = CGSize(width: size.width, height: topPanelHeight) + listInsets.top += topPanelHeight + additionalTopInset += topPanelHeight + } else if self.canReportPeer { + let topPanel: TopPanelItem + var topPanelTransition = Transition(transition) + if let current = self.topPanel { + topPanel = current + } else { + topPanelTransition = .immediate + topPanel = TopPanelItem() + self.topPanel = topPanel + } + + let title: String = self.presentationData.strings.Conversation_ReportSpamAndLeave + + let topPanelHeight: CGFloat = 44.0 + + let _ = topPanel.view.update( + transition: topPanelTransition, + component: AnyComponent(ActionPanelComponent( + theme: self.presentationData.theme, + title: title, + color: .destructive, + action: { [weak self] in + guard let self, case let .forum(peerId) = self.location else { + return + } + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: self.presentationData.strings.Conversation_ReportSpamGroupConfirmation), + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + if let self { + self.controller?.setInlineChatList(location: nil) + let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true).start() + } + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + self.listNode.present?(actionSheet) + }, + dismissAction: { [weak self] in + guard let self, case let .forum(peerId) = self.location else { + return + } + let _ = self.context.engine.peers.dismissPeerStatusOptions(peerId: peerId).start() + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: topPanelHeight) + ) + if let topPanelView = topPanel.view.view { + if topPanelView.superview == nil { + self.view.addSubview(topPanelView) + } + } + + topPanel.size = CGSize(width: size.width, height: topPanelHeight) + listInsets.top += topPanelHeight + additionalTopInset += topPanelHeight + } else { + if let topPanel = self.topPanel { + self.topPanel = nil + if let topPanelView = topPanel.view.view { + transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in + topPanelView?.removeFromSuperview() + }) + } + } + } + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) + self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, storiesInset: storiesInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction) + + if let emptyNode = self.emptyNode { + let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + transition.updateFrame(node: emptyNode, frame: emptyNodeFrame) + emptyNode.updateLayout(size: emptyNodeFrame.size, insets: listInsets, transition: transition) + + if let scrollingOffset = self.scrollingOffset { + emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: transition) + } + } + + self.layoutAdditionalPanels(transition: transition) + } + + func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) { + self.scrollingOffset = (navigationHeight, offset) + + if let emptyNode = self.emptyNode { + emptyNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition) + } + } +} diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index c9bf9c3d75..7bad9c9780 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -44,724 +44,6 @@ public enum ChatListContainerNodeFilter: Equatable { } } -private final class ShimmerEffectNode: ASDisplayNode { - private var currentBackgroundColor: UIColor? - private var currentForegroundColor: UIColor? - private let imageNodeContainer: ASDisplayNode - private let imageNode: ASImageNode - - private var absoluteLocation: (CGRect, CGSize)? - private var isCurrentlyInHierarchy = false - private var shouldBeAnimating = false - - override init() { - self.imageNodeContainer = ASDisplayNode() - self.imageNodeContainer.isLayerBacked = true - - self.imageNode = ASImageNode() - self.imageNode.isLayerBacked = true - self.imageNode.displaysAsynchronously = false - self.imageNode.displayWithoutProcessing = true - self.imageNode.contentMode = .scaleToFill - - super.init() - - self.isLayerBacked = true - self.clipsToBounds = true - - self.imageNodeContainer.addSubnode(self.imageNode) - self.addSubnode(self.imageNodeContainer) - } - - override func didEnterHierarchy() { - super.didEnterHierarchy() - - self.isCurrentlyInHierarchy = true - self.updateAnimation() - } - - override func didExitHierarchy() { - super.didExitHierarchy() - - self.isCurrentlyInHierarchy = false - self.updateAnimation() - } - - func update(backgroundColor: UIColor, foregroundColor: UIColor) { - if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { - return - } - self.currentBackgroundColor = backgroundColor - self.currentForegroundColor = foregroundColor - - self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in - context.setFillColor(backgroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - - context.clip(to: CGRect(origin: CGPoint(), size: size)) - - let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor - let peakColor = foregroundColor.cgColor - - var locations: [CGFloat] = [0.0, 0.5, 1.0] - let colors: [CGColor] = [transparentColor, peakColor, transparentColor] - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - }) - } - - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { - if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { - return - } - let sizeUpdated = self.absoluteLocation?.1 != containerSize - let frameUpdated = self.absoluteLocation?.0 != rect - self.absoluteLocation = (rect, containerSize) - - if sizeUpdated { - if self.shouldBeAnimating { - self.imageNode.layer.removeAnimation(forKey: "shimmer") - self.addImageAnimation() - } - } - - if frameUpdated { - self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) - } - - self.updateAnimation() - } - - private func updateAnimation() { - let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil - if shouldBeAnimating != self.shouldBeAnimating { - self.shouldBeAnimating = shouldBeAnimating - if shouldBeAnimating { - self.addImageAnimation() - } else { - self.imageNode.layer.removeAnimation(forKey: "shimmer") - } - } - } - - private func addImageAnimation() { - guard let containerSize = self.absoluteLocation?.1 else { - return - } - let gradientHeight: CGFloat = 250.0 - self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) - let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) - animation.repeatCount = Float.infinity - animation.beginTime = 1.0 - self.imageNode.layer.add(animation, forKey: "shimmer") - } -} - -private final class ChatListShimmerNode: ASDisplayNode { - private let backgroundColorNode: ASDisplayNode - private let effectNode: ShimmerEffectNode - private let maskNode: ASImageNode - private var currentParams: (size: CGSize, presentationData: PresentationData)? - - override init() { - self.backgroundColorNode = ASDisplayNode() - self.effectNode = ShimmerEffectNode() - self.maskNode = ASImageNode() - - super.init() - - self.isUserInteractionEnabled = false - - self.addSubnode(self.backgroundColorNode) - self.addSubnode(self.effectNode) - self.addSubnode(self.maskNode) - } - - func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { - if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { - self.currentParams = (size, presentationData) - - let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - - let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)) - let timestamp1: Int32 = 100000 - let peers: [EnginePeer.Id: EnginePeer] = [:] - 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: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) - interaction.isInlineMode = isInlineMode - - let items = (0 ..< 2).map { _ -> ChatListItem in - let message = EngineMessage( - stableId: 0, - stableVersion: 0, - id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), - globallyUniqueId: nil, - groupingKey: nil, - groupInfo: nil, - threadId: nil, - timestamp: timestamp1, - flags: [], - tags: [], - globalTags: [], - localTags: [], - forwardInfo: nil, - author: peer1, - text: "Text", - attributes: [], - media: [], - peers: peers, - associatedMessages: [:], - associatedMessageIds: [], - associatedMedia: [:], - associatedThreadInfo: nil, - associatedStories: [:] - ) - let readState = EnginePeerReadCounters() - - return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData( - messages: [message], - peer: EngineRenderedPeer(peer: peer1), - threadInfo: nil, - combinedReadState: readState, - isRemovedFromTotalUnreadCount: false, - presence: nil, - hasUnseenMentions: false, - hasUnseenReactions: false, - draftState: nil, - inputActivities: nil, - promoInfo: nil, - ignoreUnreadBadge: false, - displayAsMessage: false, - hasFailedMessages: false, - forumTopicData: nil, - topForumTopicItems: [], - autoremoveTimeout: nil, - storyState: nil - )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) - } - - var itemNodes: [ChatListItemNode] = [] - for i in 0 ..< items.count { - items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in - if let itemNode = node as? ChatListItemNode { - itemNodes.append(itemNode) - } - apply().1(ListViewItemApply(isOnScreen: true)) - }) - } - - self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor - - self.maskNode.image = generateImage(size, rotatedContext: { size, context in - context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - - var currentY: CGFloat = 0.0 - let fakeLabelPlaceholderHeight: CGFloat = 8.0 - - func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) { - let startPoint = origin - let diameter = fakeLabelPlaceholderHeight - context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) - context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) - } - - while currentY < size.height { - let sampleIndex = 0 - let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height - - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - - if !isInlineMode { - if !itemNodes[sampleIndex].avatarNode.isHidden { - context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY)) - } - } - - let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY) - if isInlineMode { - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0) - } else { - fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) - } - - let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) - - if isInlineMode { - context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0))) - } - - fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0) - - fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0) - fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0) - - let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY) - fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0) - - context.setBlendMode(.normal) - context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) - context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) - - currentY += itemHeight - } - }) - - self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)) - self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size) - } - transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - } -} - -private final class ChatListContainerItemNode: ASDisplayNode { - private final class TopPanelItem { - let view = ComponentView() - var size: CGSize? - - init() { - } - } - - private let context: AccountContext - private weak var controller: ChatListControllerImpl? - private let location: ChatListControllerLocation - private let animationCache: AnimationCache - private let animationRenderer: MultiAnimationRenderer - private var presentationData: PresentationData - private let becameEmpty: (ChatListFilter?) -> Void - private let emptyAction: (ChatListFilter?) -> Void - private let secondaryEmptyAction: () -> Void - private let openArchiveSettings: () -> Void - private let isInlineMode: Bool - - private var floatingHeaderOffset: CGFloat? - - private(set) var emptyNode: ChatListEmptyNode? - var emptyShimmerEffectNode: ChatListShimmerNode? - private var shimmerNodeOffset: CGFloat = 0.0 - let listNode: ChatListNode - - private var topPanel: TopPanelItem? - - private var pollFilterUpdatesDisposable: Disposable? - private var chatFilterUpdatesDisposable: Disposable? - private var peerDataDisposable: Disposable? - - private var chatFolderUpdates: ChatFolderUpdates? - - private var canReportPeer: Bool = false - - private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)? - private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)? - - init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) { - self.context = context - self.controller = controller - self.location = location - self.animationCache = animationCache - self.animationRenderer = animationRenderer - self.presentationData = presentationData - self.becameEmpty = becameEmpty - self.emptyAction = emptyAction - self.secondaryEmptyAction = secondaryEmptyAction - self.openArchiveSettings = openArchiveSettings - self.isInlineMode = isInlineMode - - self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady) - - if let controller, case .chatList(groupId: .root) = controller.location { - self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight - } - - super.init() - - self.addSubnode(self.listNode) - - self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in - guard let strongSelf = self else { - return - } - var needsShimmerNode = false - var shimmerNodeOffset: CGFloat = 0.0 - - var needsEmptyNode = false - var hasOnlyArchive = false - var hasOnlyGeneralThread = false - var isLoading = false - - switch isEmptyState { - case let .empty(isLoadingValue, hasArchiveInfo): - if hasArchiveInfo { - shimmerNodeOffset = 253.0 - } - if isLoadingValue { - needsShimmerNode = true - needsEmptyNode = false - isLoading = isLoadingValue - } else { - needsEmptyNode = true - } - if !isLoadingValue { - strongSelf.becameEmpty(filter) - } - case let .notEmpty(_, onlyHasArchiveValue, onlyGeneralThreadValue): - needsEmptyNode = onlyHasArchiveValue || onlyGeneralThreadValue - hasOnlyArchive = onlyHasArchiveValue - hasOnlyGeneralThread = onlyGeneralThreadValue - } - - if needsEmptyNode { - if let currentNode = strongSelf.emptyNode { - currentNode.updateIsLoading(isLoading) - } else { - let subject: ChatListEmptyNode.Subject - if let filter = filter { - var showEdit = true - if case let .filter(_, _, _, data) = filter { - if data.excludeRead && data.includePeers.peers.isEmpty && data.includePeers.pinnedPeers.isEmpty { - showEdit = false - } - } - subject = .filter(showEdit: showEdit) - } else { - if case .forum = location { - subject = .forum(hasGeneral: hasOnlyGeneralThread) - } else { - if case .chatList(groupId: .archive) = location { - subject = .archive - } else { - subject = .chats(hasArchive: hasOnlyArchive) - } - } - } - - let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: { - self?.emptyAction(filter) - }, secondaryAction: { - self?.secondaryEmptyAction() - }, openArchiveSettings: { - self?.openArchiveSettings() - }) - strongSelf.emptyNode = emptyNode - strongSelf.listNode.addSubnode(emptyNode) - if let (size, insets, _, _, _, _, _) = strongSelf.validLayout { - let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - emptyNode.frame = emptyNodeFrame - emptyNode.updateLayout(size: size, insets: insets, transition: .immediate) - - if let scrollingOffset = strongSelf.scrollingOffset { - emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate) - } - } - emptyNode.alpha = 0.0 - transition.updateAlpha(node: emptyNode, alpha: 1.0) - } - } else if let emptyNode = strongSelf.emptyNode { - strongSelf.emptyNode = nil - transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in - emptyNode?.removeFromSupernode() - }) - } - - - if needsShimmerNode { - strongSelf.shimmerNodeOffset = shimmerNodeOffset - if strongSelf.emptyShimmerEffectNode == nil { - let emptyShimmerEffectNode = ChatListShimmerNode() - strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode - strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode) - if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset { - strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate) - } - } - } else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode { - strongSelf.emptyShimmerEffectNode = nil - let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut) - emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in - emptyShimmerEffectNode?.removeFromSupernode() - }) - strongSelf.listNode.alpha = 0.0 - emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0) - } - } - - self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in - guard let strongSelf = self else { - return - } - strongSelf.floatingHeaderOffset = offset - if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode { - strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition) - } - strongSelf.layoutAdditionalPanels(transition: transition) - } - - if let filter, case let .filter(id, _, _, data) = filter, data.isShared { - 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 - } - var update = false - if let result, result.availableChatsToJoin != 0 { - if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin { - update = true - } - self.chatFolderUpdates = result - } else { - if self.chatFolderUpdates != nil { - self.chatFolderUpdates = nil - update = true - } - } - if update { - if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { - self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring)) - } - } - }) - } - - if case let .forum(peerId) = location { - self.peerDataDisposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] statusSettings in - guard let self else { - return - } - var canReportPeer = false - if let statusSettings, statusSettings.flags.contains(.canReport) { - canReportPeer = true - } - if self.canReportPeer != canReportPeer { - self.canReportPeer = canReportPeer - if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout { - self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring)) - } - } - }) - } - } - - deinit { - self.pollFilterUpdatesDisposable?.dispose() - self.chatFilterUpdatesDisposable?.dispose() - self.peerDataDisposable?.dispose() - } - - private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) { - node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, isInlineMode: self.isInlineMode, presentationData: self.presentationData, transition: .immediate) - transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size)) - } - - private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) { - guard let (size, insets, visualNavigationHeight, _, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else { - return - } - - let _ = size - let _ = insets - - if let topPanel = self.topPanel, let topPanelSize = topPanel.size { - let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height - - if let topPanelView = topPanel.view.view { - var animateIn = false - var topPanelTransition = transition - if topPanelView.bounds.isEmpty { - topPanelTransition = .immediate - animateIn = true - } - topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize)) - if animateIn { - transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height)) - } - } - } - } - - func updatePresentationData(_ presentationData: PresentationData) { - self.presentationData = presentationData - - self.listNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - - self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) - } - - func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) - - var listInsets = insets - var additionalTopInset: CGFloat = 0.0 - - if let chatFolderUpdates = self.chatFolderUpdates { - let topPanel: TopPanelItem - var topPanelTransition = Transition(transition) - if let current = self.topPanel { - topPanel = current - } else { - topPanelTransition = .immediate - topPanel = TopPanelItem() - self.topPanel = topPanel - } - - let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin)) - - let topPanelHeight: CGFloat = 44.0 - - let _ = topPanel.view.update( - transition: topPanelTransition, - component: AnyComponent(ActionPanelComponent( - theme: self.presentationData.theme, - title: title, - color: .accent, - action: { [weak self] in - guard let self, let chatFolderUpdates = self.chatFolderUpdates else { - return - } - - self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents)) - }, - dismissAction: { [weak self] in - guard let self, let chatFolderUpdates = self.chatFolderUpdates else { - return - } - let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start() - } - )), - environment: {}, - containerSize: CGSize(width: size.width, height: topPanelHeight) - ) - if let topPanelView = topPanel.view.view { - if topPanelView.superview == nil { - self.view.addSubview(topPanelView) - } - } - - topPanel.size = CGSize(width: size.width, height: topPanelHeight) - listInsets.top += topPanelHeight - additionalTopInset += topPanelHeight - } else if self.canReportPeer { - let topPanel: TopPanelItem - var topPanelTransition = Transition(transition) - if let current = self.topPanel { - topPanel = current - } else { - topPanelTransition = .immediate - topPanel = TopPanelItem() - self.topPanel = topPanel - } - - let title: String = self.presentationData.strings.Conversation_ReportSpamAndLeave - - let topPanelHeight: CGFloat = 44.0 - - let _ = topPanel.view.update( - transition: topPanelTransition, - component: AnyComponent(ActionPanelComponent( - theme: self.presentationData.theme, - title: title, - color: .destructive, - action: { [weak self] in - guard let self, case let .forum(peerId) = self.location else { - return - } - - let actionSheet = ActionSheetController(presentationData: self.presentationData) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: self.presentationData.strings.Conversation_ReportSpamGroupConfirmation), - ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - if let self { - self.controller?.setInlineChatList(location: nil) - let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true).start() - } - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.listNode.present?(actionSheet) - }, - dismissAction: { [weak self] in - guard let self, case let .forum(peerId) = self.location else { - return - } - let _ = self.context.engine.peers.dismissPeerStatusOptions(peerId: peerId).start() - } - )), - environment: {}, - containerSize: CGSize(width: size.width, height: topPanelHeight) - ) - if let topPanelView = topPanel.view.view { - if topPanelView.superview == nil { - self.view.addSubview(topPanelView) - } - } - - topPanel.size = CGSize(width: size.width, height: topPanelHeight) - listInsets.top += topPanelHeight - additionalTopInset += topPanelHeight - } else { - if let topPanel = self.topPanel { - self.topPanel = nil - if let topPanelView = topPanel.view.view { - transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in - topPanelView?.removeFromSuperview() - }) - } - } - } - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve) - - transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) - self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, storiesInset: storiesInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction) - - if let emptyNode = self.emptyNode { - let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - transition.updateFrame(node: emptyNode, frame: emptyNodeFrame) - emptyNode.updateLayout(size: emptyNodeFrame.size, insets: listInsets, transition: transition) - - if let scrollingOffset = self.scrollingOffset { - emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: transition) - } - } - - self.layoutAdditionalPanels(transition: transition) - } - - func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) { - self.scrollingOffset = (navigationHeight, offset) - - if let emptyNode = self.emptyNode { - emptyNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition) - } - } -} - public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext private weak var controller: ChatListControllerImpl? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index df56415297..7f15d49781 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2345,19 +2345,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { for item in items { switch item { case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _, _): - if case .user = peer { - storyStatsIds.append(peer.id) - } + storyStatsIds.append(peer.id) case let .localPeer(peer, _, _, _, _, _, _, _, _, _): - if case .user = peer { - storyStatsIds.append(peer.id) - } + storyStatsIds.append(peer.id) case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _): - if foundPeer.peer is TelegramUser { - storyStatsIds.append(foundPeer.peer.id) - } + storyStatsIds.append(foundPeer.peer.id) case let .message(_, peer, _, _, _, _, _, _, _, _, _, _, _): - if let peer = peer.peer, case .user = peer { + if let peer = peer.peer { storyStatsIds.append(peer.id) } default: @@ -3386,7 +3380,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } -private final class ShimmerEffectNode: ASDisplayNode { +private final class SearchShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private let imageNodeContainer: ASDisplayNode @@ -3504,13 +3498,13 @@ private final class ShimmerEffectNode: ASDisplayNode { public final class ChatListSearchShimmerNode: ASDisplayNode { private let backgroundColorNode: ASDisplayNode - private let effectNode: ShimmerEffectNode + private let effectNode: SearchShimmerEffectNode private let maskNode: ASImageNode private var currentParams: (size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey)? public init(key: ChatListSearchPaneKey) { self.backgroundColorNode = ASDisplayNode() - self.effectNode = ShimmerEffectNode() + self.effectNode = SearchShimmerEffectNode() self.maskNode = ASImageNode() super.init() diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift new file mode 100644 index 0000000000..8c2abde650 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -0,0 +1,288 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext +import AnimationCache +import MultiAnimationRenderer +import TelegramCore + +final class ShimmerEffectNode: ASDisplayNode { + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init() { + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.contentMode = .scaleToFill + + super.init() + + self.isLayerBacked = true + self.clipsToBounds = true + + self.imageNodeContainer.addSubnode(self.imageNode) + self.addSubnode(self.imageNodeContainer) + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + + self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } + } + + if frameUpdated { + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + + self.updateAnimation() + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 250.0 + self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageNode.layer.add(animation, forKey: "shimmer") + } +} + +final class ChatListShimmerNode: ASDisplayNode { + private let backgroundColorNode: ASDisplayNode + private let effectNode: ShimmerEffectNode + private let maskNode: ASImageNode + private var currentParams: (size: CGSize, presentationData: PresentationData)? + + override init() { + self.backgroundColorNode = ASDisplayNode() + self.effectNode = ShimmerEffectNode() + self.maskNode = ASImageNode() + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.backgroundColorNode) + self.addSubnode(self.effectNode) + self.addSubnode(self.maskNode) + } + + func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { + self.currentParams = (size, presentationData) + + let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)) + let timestamp1: Int32 = 100000 + let peers: [EnginePeer.Id: EnginePeer] = [:] + 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: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) + interaction.isInlineMode = isInlineMode + + let items = (0 ..< 2).map { _ -> ChatListItem in + let message = EngineMessage( + stableId: 0, + stableVersion: 0, + id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: timestamp1, + flags: [], + tags: [], + globalTags: [], + localTags: [], + forwardInfo: nil, + author: peer1, + text: "Text", + attributes: [], + media: [], + peers: peers, + associatedMessages: [:], + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + let readState = EnginePeerReadCounters() + + return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData( + messages: [message], + peer: EngineRenderedPeer(peer: peer1), + threadInfo: nil, + combinedReadState: readState, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil + )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + } + + var itemNodes: [ChatListItemNode] = [] + for i in 0 ..< items.count { + items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in + if let itemNode = node as? ChatListItemNode { + itemNodes.append(itemNode) + } + apply().1(ListViewItemApply(isOnScreen: true)) + }) + } + + self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor + + self.maskNode.image = generateImage(size, rotatedContext: { size, context in + context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + var currentY: CGFloat = 0.0 + let fakeLabelPlaceholderHeight: CGFloat = 8.0 + + func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) { + let startPoint = origin + let diameter = fakeLabelPlaceholderHeight + context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) + } + + while currentY < size.height { + let sampleIndex = 0 + let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + + if !isInlineMode { + if !itemNodes[sampleIndex].avatarNode.isHidden { + context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY)) + } + } + + let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY) + if isInlineMode { + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0) + } else { + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0) + } + + let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) + + if isInlineMode { + context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0))) + } + + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0) + + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0) + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0) + + let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0) + + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) + context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) + + currentY += itemHeight + } + }) + + self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)) + self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size) + } + transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index dc45ee810b..f281846a61 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -23,8 +23,6 @@ import ChatListHeaderComponent import UndoUI import NewSessionInfoScreen -private var debugDidAddNewSessionReview = false - public enum ChatListNodeMode { case chatList(appendContacts: Bool) case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) @@ -1637,11 +1635,11 @@ public final class ChatListNode: ListView { return true })) - let _ = removeNewSessionReviews(postbox: self.context.account.postbox, ids: [newSessionReview.id]).start() + let _ = self.context.engine.privacy.confirmNewSessionReview(id: newSessionReview.id) } else { self.push?(NewSessionInfoScreen(context: self.context, newSessionReview: newSessionReview)) - let _ = removeNewSessionReviews(postbox: self.context.account.postbox, ids: [newSessionReview.id]).start() + let _ = self.context.engine.privacy.terminateAnotherSession(id: newSessionReview.id).start() } }, openChatFolderUpdates: { [weak self] in guard let self else { @@ -1743,12 +1741,7 @@ public final class ChatListNode: ListView { let suggestedChatListNotice: Signal if case .chatList(groupId: .root) = location, chatListFilter == nil { - #if DEBUG - if !debugDidAddNewSessionReview { - debugDidAddNewSessionReview = true - let _ = addNewSessionReview(postbox: context.account.postbox, item: NewSessionReview(id: 1, device: "iPhone 14 Pro", location: "Dubai, UAE")).start() - } - #endif + let _ = context.engine.privacy.cleanupSessionReviews().start() suggestedChatListNotice = .single(nil) |> then ( @@ -2952,6 +2945,15 @@ public final class ChatListNode: ListView { } } + self.dynamicVisualInsets = { [weak self] in + guard let self else { + return UIEdgeInsets() + } + + let _ = self + return UIEdgeInsets() + } + self.pollFilterUpdates() self.resetFilter() diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 2c3d428b03..50790bea38 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -112,12 +112,12 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { self.contentContainer.clipsToBounds = true self.clipsToBounds = true - self.addSubnode(self.separatorNode) self.contentContainer.addSubnode(self.titleNode) self.contentContainer.addSubnode(self.textNode) self.contentContainer.addSubnode(self.arrowNode) self.addSubnode(self.contentContainer) + self.addSubnode(self.separatorNode) self.zPosition = 1.0 } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 6672f43915..c0b1854353 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -189,6 +189,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture public private(set) final var visibleSize: CGSize = CGSize() public private(set) final var insets = UIEdgeInsets() public final var visualInsets: UIEdgeInsets? + public final var dynamicVisualInsets: (() -> UIEdgeInsets)? public private(set) final var headerInsets = UIEdgeInsets() public private(set) final var scrollIndicatorInsets = UIEdgeInsets() private final var ensureTopInsetForOverlayHighlightedItems: CGFloat? @@ -4388,7 +4389,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if abs(apparentHeightDelta) > CGFloat.ulpOfOne { itemNode.updateFrame(itemNode.frame, within: self.visibleSize) - let visualInsets = self.visualInsets ?? self.insets + let visualInsets = self.dynamicVisualInsets?() ?? self.visualInsets ?? self.insets if itemNode.apparentFrame.maxY <= visualInsets.top { offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 4a9ea3b17d..1d919f2d0f 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -20,7 +20,7 @@ private let rankMaxLength: Int32 = 16 private final class ChannelAdminControllerArguments { let context: AccountContext let updateAdminRights: (Bool) -> Void - let toggleRight: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void + let toggleRight: (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void let toggleRightWhileDisabled: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let transferOwnership: () -> Void let updateRank: (String, String) -> Void @@ -28,8 +28,9 @@ private final class ChannelAdminControllerArguments { let dismissAdmin: () -> Void let dismissInput: () -> Void let animateError: () -> Void + let toggleIsOptionExpanded: (RightsItem.Sub) -> Void - init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { + init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void, toggleIsOptionExpanded: @escaping (RightsItem.Sub) -> Void) { self.context = context self.updateAdminRights = updateAdminRights self.toggleRight = toggleRight @@ -40,6 +41,7 @@ private final class ChannelAdminControllerArguments { self.dismissAdmin = dismissAdmin self.dismissInput = dismissInput self.animateError = animateError + self.toggleIsOptionExpanded = toggleIsOptionExpanded } } @@ -71,12 +73,41 @@ private enum ChannelAdminEntryStableId: Hashable { case rankInfo case adminRights case rightsTitle - case right(TelegramChatAdminRightsFlags) + case right(RightsItem) case addAdminsInfo case transfer case dismiss } +private struct AdminSubPermission: Equatable { + var title: String + var flags: TelegramChatAdminRightsFlags + var isSelected: Bool + var isEnabled: Bool +} + +enum RightsItem: Equatable, Hashable { + enum Sub { + case messages + case stories + } + + case direct(TelegramChatAdminRightsFlags) + case sub(Sub, [TelegramChatAdminRightsFlags]) +} + +private let messageRelatedFlags: [TelegramChatAdminRightsFlags] = [ + .canPostMessages, + .canEditMessages, + .canDeleteMessages +] + +private let storiesRelatedFlags: [TelegramChatAdminRightsFlags] = [ + .canPostStories, + .canEditStories, + .canDeleteStories +] + private enum ChannelAdminEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, EnginePeer.Presence?) case rankTitle(PresentationTheme, String, Int32?, Int32) @@ -84,7 +115,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { case rankInfo(PresentationTheme, String, Bool) case adminRights(PresentationTheme, String, Bool) case rightsTitle(PresentationTheme, String) - case rightItem(PresentationTheme, Int, String, TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags, Bool, Bool) + case rightItem(PresentationTheme, Int, String, RightsItem, TelegramChatAdminRightsFlags, Bool, Bool, [AdminSubPermission], Bool) case addAdminsInfo(PresentationTheme, String) case transfer(PresentationTheme, String) case dismiss(PresentationTheme, String) @@ -120,7 +151,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return .adminRights case .rightsTitle: return .rightsTitle - case let .rightItem(_, _, _, right, _, _, _): + case let .rightItem(_, _, _, right, _, _, _, _, _): return .right(right) case .addAdminsInfo: return .addAdminsInfo @@ -185,8 +216,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } else { return false } - case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled): - if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs { + case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled, lhsSubItems, lhsIsExpanded): + if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled, rhsSubItems, rhsIsExpanded) = rhs { if lhsTheme !== rhsTheme { return false } @@ -208,6 +239,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { if lhsEnabled != rhsEnabled { return false } + if lhsSubItems != rhsSubItems { + return false + } + if lhsIsExpanded != rhsIsExpanded { + return false + } return true } else { return false @@ -256,11 +293,11 @@ private enum ChannelAdminEntry: ItemListNodeEntry { default: return true } - case let .rightItem(_, lhsIndex, _, _, _, _, _): + case let .rightItem(_, lhsIndex, _, _, _, _, _, _, _): switch rhs { case .info, .adminRights, .rightsTitle: return false - case let .rightItem(_, rhsIndex, _, _, _, _, _): + case let .rightItem(_, rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex default: return true @@ -341,12 +378,48 @@ private enum ChannelAdminEntry: ItemListNodeEntry { }) case let .rightsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .rightItem(_, _, text, right, flags, value, enabled): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in - arguments.toggleRight(right, flags) - }, activatedWhileDisabled: { - arguments.toggleRightWhileDisabled(right, flags) - }) + case let .rightItem(_, _, text, right, flags, value, enabled, subPermissions, isExpanded): + if !subPermissions.isEmpty { + return ItemListExpandableSwitchItem(presentationData: presentationData, title: text, value: value, isExpanded: isExpanded, subItems: subPermissions.map { item in + return ItemListExpandableSwitchItem.SubItem( + id: AnyHashable(item.flags.rawValue), + title: item.title, + isSelected: item.isSelected, + isEnabled: item.isEnabled + ) + }, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + if enabled { + arguments.toggleRight(right, flags, value) + } else { + //arguments.toggleRightWhileDisabled(right, flags) + } + }, activatedWhileDisabled: { + //arguments.toggleRightWhileDisabled(right, flags) + }, selectAction: { + if case let .sub(type, _) = right { + arguments.toggleIsOptionExpanded(type) + } + }, subAction: { item in + guard let value = item.id.base as? Int32 else { + return + } + let subRights = TelegramChatAdminRightsFlags(rawValue: value) + + if enabled { + arguments.toggleRight(.direct(subRights), flags, !item.isSelected) + } else { + arguments.toggleRightWhileDisabled(subRights, flags) + } + }) + } else { + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleRight(right, flags, value) + }, activatedWhileDisabled: { + if case let .direct(right) = right { + arguments.toggleRightWhileDisabled(right, flags) + } + }) + } case let .addAdminsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .transfer(_, text): @@ -362,18 +435,20 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } private struct ChannelAdminControllerState: Equatable { - let adminRights: Bool - let updatedFlags: TelegramChatAdminRightsFlags? - let updatedRank: String? - let updating: Bool - let focusedOnRank: Bool + var adminRights: Bool + var updatedFlags: TelegramChatAdminRightsFlags? + var updatedRank: String? + var updating: Bool + var focusedOnRank: Bool + var expandedPermissions: Set = Set() - init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false) { + init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false, expandedPermissions: Set = Set()) { self.adminRights = adminRights self.updatedFlags = updatedFlags self.updatedRank = updatedRank self.updating = updating self.focusedOnRank = focusedOnRank + self.expandedPermissions = expandedPermissions } static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool { @@ -392,27 +467,30 @@ private struct ChannelAdminControllerState: Equatable { if lhs.focusedOnRank != rhs.focusedOnRank { return false } + if lhs.expandedPermissions != rhs.expandedPermissions { + return false + } return true } func withUpdatedAdminRights(_ adminRights: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChatAdminRightsFlags?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedUpdatedRank(_ updatedRank: String?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedFocusedOnRank(_ focusedOnRank: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank, expandedPermissions: self.expandedPermissions) } } @@ -422,7 +500,12 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm } else if right.contains(.canPostMessages) { return strings.Channel_EditAdmin_PermissionPostMessages } else if right.contains(.canEditMessages) { - return strings.Channel_EditAdmin_PermissionEditMessages + if isChannel { + //TODO:localize + return "Edit Messages of Others" + } else { + return strings.Channel_EditAdmin_PermissionEditMessages + } } else if right.contains(.canDeleteMessages) { return isGroup ? strings.Channel_EditAdmin_PermissionDeleteMessages : strings.Channel_EditAdmin_PermissionDeleteMessagesOfOthers } else if right.contains(.canBanUsers) { @@ -451,6 +534,15 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm } else { return strings.Channel_AdminLog_CanManageCalls } + } else if right.contains(.canPostStories) { + //TODO:localize + return "Post Stories" + } else if right.contains(.canEditStories) { + //TODO:localize + return "Edit Stories of Others" + } else if right.contains(.canDeleteStories) { + //TODO:localize + return "Delete Stories of Others" } else { return "" } @@ -553,45 +645,45 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let isGroup: Bool var maskRightsFlags: TelegramChatAdminRightsFlags - let rightsOrder: [TelegramChatAdminRightsFlags] + + let rightsOrder: [RightsItem] maskRightsFlags = TelegramChatAdminRightsFlags.peerSpecific(peer: .channel(channel)) switch channel.info { case .broadcast: isGroup = false rightsOrder = [ - .canChangeInfo, - .canPostMessages, - .canEditMessages, - .canDeleteMessages, - .canInviteUsers, - .canManageCalls, - .canAddAdmins + .direct(.canChangeInfo), + .sub(.messages, messageRelatedFlags), + .sub(.stories, storiesRelatedFlags), + .direct(.canInviteUsers), + .direct(.canManageCalls), + .direct(.canAddAdmins) ] case .group: isGroup = true if channel.flags.contains(.isForum) { rightsOrder = [ - .canChangeInfo, - .canDeleteMessages, - .canBanUsers, - .canInviteUsers, - .canPinMessages, - .canManageTopics, - .canManageCalls, - .canBeAnonymous, - .canAddAdmins + .direct(.canChangeInfo), + .direct(.canDeleteMessages), + .direct(.canBanUsers), + .direct(.canInviteUsers), + .direct(.canPinMessages), + .direct(.canManageTopics), + .direct(.canManageCalls), + .direct(.canBeAnonymous), + .direct(.canAddAdmins) ] } else { rightsOrder = [ - .canChangeInfo, - .canDeleteMessages, - .canBanUsers, - .canInviteUsers, - .canPinMessages, - .canManageCalls, - .canBeAnonymous, - .canAddAdmins + .direct(.canChangeInfo), + .direct(.canDeleteMessages), + .direct(.canBanUsers), + .direct(.canInviteUsers), + .direct(.canPinMessages), + .direct(.canManageCalls), + .direct(.canBeAnonymous), + .direct(.canAddAdmins) ] } } @@ -621,11 +713,52 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } var index = 0 - for right in rightsOrder { - if accountUserRightsFlags.contains(right) { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), right == .canBeAnonymous)) - index += 1 + rightsLoop: for right in rightsOrder { + let enabled: Bool + let isSelected: Bool + let itemTitle: String + var subItems: [AdminSubPermission] = [] + var isExpanded = false + + switch right { + case let .direct(right): + if !accountUserRightsFlags.contains(right) { + continue rightsLoop + } + + enabled = right == .canBeAnonymous + + itemTitle = stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights) + isSelected = currentRightsFlags.contains(right) + case let .sub(type, subRights): + let filteredSubRights = subRights.filter({ accountUserRightsFlags.contains($0) }) + if filteredSubRights.isEmpty { + continue rightsLoop + } + + enabled = true + + //TODO:localize + switch type { + case .messages: + itemTitle = "Manage Messages" + case .stories: + itemTitle = "Manage Stories" + } + + isSelected = subRights.allSatisfy({ currentRightsFlags.contains($0) }) + + isExpanded = state.expandedPermissions.contains(type) + + for subRight in filteredSubRights { + let subRightEnabled = true + + subItems.append(AdminSubPermission(title: stringForRight(strings: presentationData.strings, right: subRight, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), flags: subRight, isSelected: currentRightsFlags.contains(subRight), isEnabled: enabled && subRightEnabled)) + } } + + entries.append(.rightItem(presentationData.theme, index, itemTitle, right, currentRightsFlags, isSelected, enabled, subItems, isExpanded)) + index += 1 } } } else { @@ -660,11 +793,52 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } var index = 0 - for right in rightsOrder { - if accountUserRightsFlags.contains(right) { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: .channel(channel), right: right))) - index += 1 + rightsLoop: for right in rightsOrder { + let enabled: Bool + let isSelected: Bool + let itemTitle: String + var subItems: [AdminSubPermission] = [] + var isExpanded = false + + switch right { + case let .direct(right): + if !accountUserRightsFlags.contains(right) { + continue rightsLoop + } + + enabled = !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: .channel(channel), right: right) + + itemTitle = stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights) + isSelected = currentRightsFlags.contains(right) + case let .sub(type, subRights): + let filteredSubRights = subRights.filter({ accountUserRightsFlags.contains($0) }) + if filteredSubRights.isEmpty { + continue rightsLoop + } + + enabled = !state.updating + + //TODO:localize + switch type { + case .messages: + itemTitle = "Manage Messages" + case .stories: + itemTitle = "Manage Stories" + } + + isSelected = subRights.allSatisfy({ currentRightsFlags.contains($0) }) + + isExpanded = state.expandedPermissions.contains(type) + + for subRight in filteredSubRights { + let subRightEnabled = !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: .channel(channel), right: subRight) + + subItems.append(AdminSubPermission(title: stringForRight(strings: presentationData.strings, right: subRight, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), flags: subRight, isSelected: currentRightsFlags.contains(subRight), isEnabled: enabled && subRightEnabled)) + } } + + entries.append(.rightItem(presentationData.theme, index, itemTitle, right, currentRightsFlags, isSelected, enabled, subItems, isExpanded)) + index += 1 } if accountUserRightsFlags.contains(.canAddAdmins) { @@ -693,8 +867,43 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { var index = 0 - for right in rightsOrder { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), right, adminInfo.rights.rights, adminInfo.rights.rights.contains(right), false)) + rightsLoop: for right in rightsOrder { + let enabled: Bool = false + let isSelected: Bool + let itemTitle: String + var subItems: [AdminSubPermission] = [] + var isExpanded = false + + switch right { + case let .direct(right): + itemTitle = stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights) + isSelected = adminInfo.rights.rights.contains(right) + case let .sub(type, subRights): + let filteredSubRights = subRights + if filteredSubRights.isEmpty { + continue rightsLoop + } + + //TODO:localize + switch type { + case .messages: + itemTitle = "Manage Messages" + case .stories: + itemTitle = "Manage Stories" + } + + isSelected = subRights.allSatisfy({ adminInfo.rights.rights.contains($0) }) + + isExpanded = state.expandedPermissions.contains(type) + + for subRight in filteredSubRights { + let subRightEnabled = false + + subItems.append(AdminSubPermission(title: stringForRight(strings: presentationData.strings, right: subRight, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), flags: subRight, isSelected: adminInfo.rights.rights.contains(subRight), isEnabled: enabled && subRightEnabled)) + } + } + + entries.append(.rightItem(presentationData.theme, index, itemTitle, right, adminInfo.rights.rights, isSelected, enabled, subItems, isExpanded)) index += 1 } } @@ -792,7 +1001,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s var index = 0 for right in rightsOrder { if accountUserRightsFlags.contains(right) { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: false, defaultBannedRights: group.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && accountIsCreator)) + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: false, defaultBannedRights: group.defaultBannedRights), .direct(right), currentRightsFlags, currentRightsFlags.contains(right), !state.updating && accountIsCreator, [], false)) index += 1 } } @@ -866,13 +1075,25 @@ public func channelAdminController(context: AccountContext, updatedPresentationD updateState { current in return current.withUpdatedAdminRights(value) } - }, toggleRight: { right, flags in + }, toggleRight: { right, flags, value in updateState { current in var updated = flags - if flags.contains(right) { - updated.remove(right) + + var combinedRight: TelegramChatAdminRightsFlags + switch right { + case let .direct(right): + combinedRight = right + case let .sub(_, right): + combinedRight = [] + for flag in right { + combinedRight.insert(flag) + } + } + + if !value { + updated.remove(combinedRight) } else { - updated.insert(right) + updated.insert(combinedRight) } return current.withUpdatedUpdatedFlags(updated) } @@ -971,6 +1192,18 @@ public func channelAdminController(context: AccountContext, updatedPresentationD dismissInputImpl?() }, animateError: { errorImpl?() + }, toggleIsOptionExpanded: { flag in + updateState { state in + var state = state + + if state.expandedPermissions.contains(flag) { + state.expandedPermissions.remove(flag) + } else { + state.expandedPermissions.insert(flag) + } + + return state + } }) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 88ecb22169..500ed69c93 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -160,7 +160,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-531931925] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsMentions($0) } dict[-566281095] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsRecent($0) } dict[106343499] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsSearch($0) } - dict[-2094689180] = { return Api.Chat.parse_channel($0) } + dict[-1795845413] = { return Api.Chat.parse_channel($0) } dict[399807445] = { return Api.Chat.parse_channelForbidden($0) } dict[1103884886] = { return Api.Chat.parse_chat($0) } dict[693512293] = { return Api.Chat.parse_chatEmpty($0) } @@ -911,7 +911,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1461528386] = { return Api.Update.parse_updateReadFeaturedStickers($0) } dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) } dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) } - dict[1757493555] = { return Api.Update.parse_updateReadMessagesContents($0) } + dict[-131960447] = { return Api.Update.parse_updateReadMessagesContents($0) } dict[-145845461] = { return Api.Update.parse_updateReadStories($0) } dict[821314523] = { return Api.Update.parse_updateRecentEmojiStatuses($0) } dict[1870160884] = { return Api.Update.parse_updateRecentReactions($0) } diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 986abc2f91..5baa2dc1c4 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -1165,7 +1165,7 @@ public extension Api { case updateReadFeaturedStickers case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32) case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32) - case updateReadMessagesContents(messages: [Int32], pts: Int32, ptsCount: Int32) + case updateReadMessagesContents(flags: Int32, messages: [Int32], pts: Int32, ptsCount: Int32, date: Int32?) case updateReadStories(peer: Api.Peer, maxId: Int32) case updateRecentEmojiStatuses case updateRecentReactions @@ -2007,10 +2007,11 @@ public extension Api { serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) break - case .updateReadMessagesContents(let messages, let pts, let ptsCount): + case .updateReadMessagesContents(let flags, let messages, let pts, let ptsCount, let date): if boxed { - buffer.appendInt32(1757493555) + buffer.appendInt32(-131960447) } + serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { @@ -2018,6 +2019,7 @@ public extension Api { } serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(date!, buffer: buffer, boxed: false)} break case .updateReadStories(let peer, let maxId): if boxed { @@ -2384,8 +2386,8 @@ public extension Api { return ("updateReadHistoryInbox", [("flags", flags as Any), ("folderId", folderId as Any), ("peer", peer as Any), ("maxId", maxId as Any), ("stillUnreadCount", stillUnreadCount as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateReadHistoryOutbox(let peer, let maxId, let pts, let ptsCount): return ("updateReadHistoryOutbox", [("peer", peer as Any), ("maxId", maxId as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) - case .updateReadMessagesContents(let messages, let pts, let ptsCount): - return ("updateReadMessagesContents", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) + case .updateReadMessagesContents(let flags, let messages, let pts, let ptsCount, let date): + return ("updateReadMessagesContents", [("flags", flags as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any), ("date", date as Any)]) case .updateReadStories(let peer, let maxId): return ("updateReadStories", [("peer", peer as Any), ("maxId", maxId as Any)]) case .updateRecentEmojiStatuses: @@ -4116,19 +4118,25 @@ public extension Api { } } public static func parse_updateReadMessagesContents(_ reader: BufferReader) -> Update? { - var _1: [Int32]? + var _1: Int32? + _1 = reader.readInt32() + var _2: [Int32]? if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } - var _2: Int32? - _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.Update.updateReadMessagesContents(messages: _1!, pts: _2!, ptsCount: _3!) + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Update.updateReadMessagesContents(flags: _1!, messages: _2!, pts: _3!, ptsCount: _4!, date: _5) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 3d2b7eaf8e..e715e24eeb 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -522,7 +522,7 @@ public extension Api { } public extension Api { indirect enum Chat: TypeConstructorDescription { - case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?) + case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?, storiesMaxId: Int32?) case channelForbidden(flags: Int32, id: Int64, accessHash: Int64, title: String, untilDate: Int32?) case chat(flags: Int32, id: Int64, title: String, photo: Api.ChatPhoto, participantsCount: Int32, date: Int32, version: Int32, migratedTo: Api.InputChannel?, adminRights: Api.ChatAdminRights?, defaultBannedRights: Api.ChatBannedRights?) case chatEmpty(id: Int64) @@ -530,9 +530,9 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames): + case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId): if boxed { - buffer.appendInt32(-2094689180) + buffer.appendInt32(-1795845413) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -556,6 +556,7 @@ public extension Api { for item in usernames! { item.serialize(buffer, true) }} + if Int(flags2) & Int(1 << 4) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)} break case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): if boxed { @@ -600,8 +601,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames): - return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any)]) + case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId): + return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any)]) case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): return ("channelForbidden", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("untilDate", untilDate as Any)]) case .chat(let flags, let id, let title, let photo, let participantsCount, let date, let version, let migratedTo, let adminRights, let defaultBannedRights): @@ -654,6 +655,8 @@ public extension Api { if Int(_2!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self) } } + var _15: Int32? + if Int(_2!) & Int(1 << 4) != 0 {_15 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -668,8 +671,9 @@ public extension Api { let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil let _c13 = (Int(_1!) & Int(1 << 17) == 0) || _13 != nil let _c14 = (Int(_2!) & Int(1 << 0) == 0) || _14 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { - return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14) + let _c15 = (Int(_2!) & Int(1 << 4) == 0) || _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15) } else { return nil diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 79b4f16fca..b3a56674a9 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -124,6 +124,7 @@ enum AccountStateMutationOperation { case UpdateReadStories(peerId: PeerId, maxId: Int32) case UpdateStoryStealthMode(data: Api.StoriesStealthMode) case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) + case UpdateNewAuthorization(isUnconfirmed: Bool, hash: Int64, date: Int32, device: String, location: String) } struct HoleFromPreviousState { @@ -467,7 +468,7 @@ struct AccountMutableState { for chat in chats { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _): if let participantsCount = participantsCount { self.addOperation(.UpdateCachedPeerData(chat.peerId, { current in var previous: CachedChannelData @@ -653,9 +654,13 @@ struct AccountMutableState { self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction)) } + mutating func updateNewAuthorization(isUnconfirmed: Bool, hash: Int64, date: Int32, device: String, location: String) { + self.addOperation(.UpdateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date, device: device, location: location)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization: break case let .AddMessages(messages, location): for message in messages { diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 3475948d4a..9b06580655 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -61,7 +61,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .chatForbidden(id, title): return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) - case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames): + case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _): let isMin = (flags & (1 << 12)) != 0 let participationStatus: TelegramChannelParticipationStatus @@ -170,7 +170,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { switch rhs { case .chat, .chatEmpty, .chatForbidden, .channelForbidden: return parseTelegramGroupOrChannel(chat: rhs) - case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames): + case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _): let isMin = (flags & (1 << 12)) != 0 if accessHash != nil && !isMin { return parseTelegramGroupOrChannel(chat: rhs) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift index 5fbc467e5a..8e82bd168b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift @@ -18,6 +18,9 @@ public enum TelegramChannelPermission { case changeInfo case canBeAnonymous case manageCalls + case postStories + case editStories + case deleteStories } public extension TelegramChannel { @@ -235,6 +238,24 @@ public extension TelegramChannel { return true } return false + case .postStories: + if let adminRights = self.adminRights { + return adminRights.rights.contains(.canPostStories) + } else { + return false + } + case .editStories: + if let adminRights = self.adminRights { + return adminRights.rights.contains(.canEditStories) + } else { + return false + } + case .deleteStories: + if let adminRights = self.adminRights { + return adminRights.rights.contains(.canDeleteStories) + } else { + return false + } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index cf3096fab3..9f9c4b0f0c 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1479,7 +1479,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: case let .updateChannelPinnedTopic(flags, channelId, topicId): let isPinned = (flags & (1 << 0)) != 0 updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned) - case let .updateReadMessagesContents(messages, _, _): + case let .updateReadMessagesContents(_, messages, _, _, _): updatedState.addReadMessagesContents((nil, messages)) case let .updateChannelReadMessagesContents(_, channelId, topMsgId, messages): let _ = topMsgId @@ -1681,6 +1681,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateStoryStealthMode(stealthMode) case let .updateSentStoryReaction(peerId, storyId, reaction): updatedState.updateStorySentReaction(peerId: peerId.peerId, id: storyId, reaction: reaction) + case let .updateNewAuthorization(flags, hash, date, device, location): + let isUnconfirmed = (flags & (1 << 0)) != 0 + updatedState.updateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date ?? 0, device: device ?? "", location: location ?? "") default: break } @@ -3169,7 +3172,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddScheduledMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -4586,6 +4589,7 @@ func replayFinalState( isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: updatedReaction )) if let entry = CodableEntry(updatedItem) { @@ -4616,6 +4620,7 @@ func replayFinalState( isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: MessageReaction.Reaction(apiReaction: reaction) )) if let entry = CodableEntry(updatedItem) { @@ -4623,6 +4628,20 @@ func replayFinalState( storyUpdates.append(InternalStoryUpdate.added(peerId: peerId, item: updatedItem)) } } + case let .UpdateNewAuthorization(isUnconfirmed, hash, date, device, location): + let id = NewSessionReview.Id(id: hash) + if isUnconfirmed { + if let entry = CodableEntry(NewSessionReview( + id: hash, + device: device, + location: location, + timestamp: date + )) { + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, item: OrderedItemListEntry(id: id.rawValue, contents: entry), removeTailIfCountExceeds: 200) + } + } else { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, itemId: id.rawValue) + } } } diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 88d9054a43..723224a78f 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -7,6 +7,7 @@ import MtProtoKit struct AccumulatedPeers { var peers: [PeerId: Peer] = [:] var users: [PeerId: Api.User] = [:] + var chats: [PeerId: Api.Chat] = [:] var allIds: Set { var result = Set() @@ -31,6 +32,9 @@ struct AccumulatedPeers { for user in users { self.users[user.peerId] = user } + for chat in chats { + self.chats[chat.peerId] = chat + } } init(chats: [Api.Chat], users: [Api.User]) { @@ -42,6 +46,9 @@ struct AccumulatedPeers { for user in users { self.users[user.peerId] = user } + for chat in chats { + self.chats[chat.peerId] = chat + } } init(users: [Api.User]) { @@ -65,6 +72,9 @@ struct AccumulatedPeers { for (id, user) in other.users { result.users[id] = user } + for (id, chat) in other.chats { + result.chats[id] = chat + } return result } diff --git a/submodules/TelegramCore/Sources/State/UpdateGroup.swift b/submodules/TelegramCore/Sources/State/UpdateGroup.swift index 85ac1f8424..2cd052db2c 100644 --- a/submodules/TelegramCore/Sources/State/UpdateGroup.swift +++ b/submodules/TelegramCore/Sources/State/UpdateGroup.swift @@ -70,7 +70,7 @@ func apiUpdatePtsRange(_ update: Api.Update) -> (Int32, Int32)? { return (pts, ptsCount) case let .updateEditMessage(_, pts, ptsCount): return (pts, ptsCount) - case let .updateReadMessagesContents(_, pts, ptsCount): + case let .updateReadMessagesContents(_, _, pts, ptsCount, _): return (pts, ptsCount) case let .updateWebPage(_, pts, ptsCount): return (pts, ptsCount) diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 2534378307..0974d8e646 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -181,7 +181,7 @@ extension Api.Chat { return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) case let .chatForbidden(id, _): return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) - case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _): + case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) case let .channelForbidden(_, id, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewSessionReview.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewSessionReview.swift index e999641dfc..71e3094910 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewSessionReview.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewSessionReview.swift @@ -1,6 +1,7 @@ import Foundation import Postbox import SwiftSignalKit +import TelegramApi public final class NewSessionReview: Codable, Equatable { struct Id { @@ -19,11 +20,13 @@ public final class NewSessionReview: Codable, Equatable { public let id: Int64 public let device: String public let location: String + public let timestamp: Int32 - public init(id: Int64, device: String, location: String) { + public init(id: Int64, device: String, location: String, timestamp: Int32) { self.id = id self.device = device self.location = location + self.timestamp = timestamp } public init(from decoder: Decoder) throws { @@ -32,6 +35,7 @@ public final class NewSessionReview: Codable, Equatable { self.id = try container.decode(Int64.self, forKey: "id") self.device = try container.decode(String.self, forKey: "device") self.location = try container.decode(String.self, forKey: "location") + self.timestamp = try container.decode(Int32.self, forKey: "timestamp") } public func encode(to encoder: Encoder) throws { @@ -40,6 +44,7 @@ public final class NewSessionReview: Codable, Equatable { try container.encode(self.id, forKey: "id") try container.encode(self.device, forKey: "device") try container.encode(self.location, forKey: "location") + try container.encode(self.timestamp, forKey: "timestamp") } public static func ==(lhs: NewSessionReview, rhs: NewSessionReview) -> Bool { @@ -52,10 +57,42 @@ public final class NewSessionReview: Codable, Equatable { if lhs.location != rhs.location { return false } + if lhs.timestamp != rhs.timestamp { + return false + } return true } } +func _internal_cleanupSessionReviews(account: Account) -> Signal { + return account.postbox.transaction { transaction -> Void in + var autoconfirmTimeout: Int32 = 7 * 24 * 60 * 60 + let appConfig = currentAppConfiguration(transaction: transaction) + if let data = appConfig.data { + if let value = data["authorization_autoconfirm_period"] as? Double { + autoconfirmTimeout = Int32(round(value)) + } + } + + let timestamp = Int32(Date().timeIntervalSince1970) + var removeIds: [MemoryBuffer] = [] + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.NewSessionReviews) { + guard let item = entry.contents.get(NewSessionReview.self) else { + removeIds.append(entry.id) + continue + } + if item.timestamp <= timestamp - autoconfirmTimeout { + removeIds.append(entry.id) + } + } + + for removeId in removeIds { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, itemId: removeId) + } + } + |> ignoreValues +} + public func newSessionReviews(postbox: Postbox) -> Signal<[NewSessionReview], NoError> { let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.NewSessionReviews) return postbox.combinedView(keys: [viewKey]) @@ -102,3 +139,11 @@ public func removeNewSessionReviews(postbox: Postbox, ids: [Int64]) -> Signal ignoreValues } + +func _internal_confirmNewSessionReview(account: Account, id: Int64) -> Signal { + return account.network.request(Api.functions.account.changeAuthorizationSettings(flags: 1 << 3, hash: id, encryptedRequestsDisabled: nil, callRequestsDisabled: nil)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatAdminRights.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatAdminRights.swift index 86c9000927..6cf6b7ff00 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatAdminRights.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatAdminRights.swift @@ -22,13 +22,17 @@ public struct TelegramChatAdminRightsFlags: OptionSet, Hashable { public static let canBeAnonymous = TelegramChatAdminRightsFlags(rawValue: 1 << 10) public static let canManageCalls = TelegramChatAdminRightsFlags(rawValue: 1 << 11) public static let canManageTopics = TelegramChatAdminRightsFlags(rawValue: 1 << 13) + public static let canPostStories = TelegramChatAdminRightsFlags(rawValue: 1 << 14) + public static let canEditStories = TelegramChatAdminRightsFlags(rawValue: 1 << 15) + public static let canDeleteStories = TelegramChatAdminRightsFlags(rawValue: 1 << 16) + public static var all: TelegramChatAdminRightsFlags { - return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canBeAnonymous, .canManageCalls, .canManageTopics] + return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canBeAnonymous, .canManageCalls, .canManageTopics, .canPostStories, .canEditStories, .canDeleteStories] } public static var allChannel: TelegramChatAdminRightsFlags { - return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canManageCalls, .canManageTopics] + return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canManageCalls, .canManageTopics, .canPostStories, .canEditStories, .canDeleteStories] } public static let internal_groupSpecific: TelegramChatAdminRightsFlags = [ @@ -49,7 +53,10 @@ public struct TelegramChatAdminRightsFlags: OptionSet, Hashable { .canDeleteMessages, .canManageCalls, .canInviteUsers, - .canAddAdmins + .canAddAdmins, + .canPostStories, + .canEditStories, + .canDeleteStories ] public static func peerSpecific(peer: EnginePeer) -> TelegramChatAdminRightsFlags { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 699e561906..257a20326f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -2254,7 +2254,7 @@ func _internal_groupCallDisplayAsAvailablePeers(accountPeerId: PeerId, network: for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index 1549d52b4a..c8fd5fee1c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -375,6 +375,7 @@ public final class EngineStoryViewListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { @@ -411,6 +412,7 @@ public final class EngineStoryViewListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index 457b179e8d..3fed816e2f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -138,7 +138,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, for chat in chats { if let groupOrChannel = parsedPeers.get(chat.peerId) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 99466c7361..670c7ebd37 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -180,6 +180,7 @@ public enum Stories { case isSelectedContacts case isForwardingDisabled case isEdited + case isMy case myReaction } @@ -200,6 +201,7 @@ public enum Stories { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool + public let isMy: Bool public let myReaction: MessageReaction.Reaction? public init( @@ -220,6 +222,7 @@ public enum Stories { isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, + isMy: Bool, myReaction: MessageReaction.Reaction? ) { self.id = id @@ -239,6 +242,7 @@ public enum Stories { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited + self.isMy = isMy self.myReaction = myReaction } @@ -268,6 +272,7 @@ public enum Stories { self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false + self.isMy = try container.decodeIfPresent(Bool.self, forKey: .isMy) ?? false self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction) } @@ -298,6 +303,7 @@ public enum Stories { try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isEdited, forKey: .isEdited) + try container.encode(self.isMy, forKey: .isMy) try container.encodeIfPresent(self.myReaction, forKey: .myReaction) } @@ -1025,6 +1031,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { @@ -1204,6 +1211,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { @@ -1232,6 +1240,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { @@ -1406,6 +1415,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { @@ -1433,6 +1443,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) updatedItems.append(updatedItem) @@ -1575,6 +1586,13 @@ extension Stories.StoredItem { mergedMyReaction = sentReaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) } + var mergedIsMy: Bool + if isMin, let existingItem = existingItem { + mergedIsMy = existingItem.isMy + } else { + mergedIsMy = (flags & (1 << 16)) != 0 + } + let item = Stories.Item( id: id, timestamp: date, @@ -1593,6 +1611,7 @@ extension Stories.StoredItem { isSelectedContacts: isSelectedContacts, isForwardingDisabled: isForwardingDisabled, isEdited: isEdited, + isMy: mergedIsMy, myReaction: mergedMyReaction ) self = .item(item) @@ -2046,6 +2065,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: reaction )) updatedItemValue = updatedItem @@ -2076,6 +2096,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: reaction )) updatedItemValue = updatedItem diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 9a62305449..9a6abb17d3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -69,9 +69,10 @@ public final class EngineStoryItem: Equatable { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool + public let isMy: Bool public let myReaction: MessageReaction.Reaction? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, myReaction: MessageReaction.Reaction?) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -90,6 +91,7 @@ public final class EngineStoryItem: Equatable { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited + self.isMy = isMy self.myReaction = myReaction } @@ -148,6 +150,9 @@ public final class EngineStoryItem: Equatable { if lhs.isEdited != rhs.isEdited { return false } + if lhs.isMy != rhs.isMy { + return false + } if lhs.myReaction != rhs.myReaction { return false } @@ -189,6 +194,7 @@ extension EngineStoryItem { isSelectedContacts: self.isSelectedContacts, isForwardingDisabled: self.isForwardingDisabled, isEdited: self.isEdited, + isMy: self.isMy, myReaction: self.myReaction ) } @@ -563,6 +569,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) items.append(mappedItem) @@ -693,6 +700,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) storyItems.append(mappedItem) @@ -847,6 +855,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) finalUpdatedState = updatedState @@ -892,6 +901,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) finalUpdatedState = updatedState @@ -939,6 +949,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction )) updatedState.items.sort(by: { lhs, rhs in @@ -982,6 +993,7 @@ public final class PeerStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction )) updatedState.items.sort(by: { lhs, rhs in @@ -1149,6 +1161,7 @@ public final class PeerExpiringStoryListContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) items.append(.item(mappedItem)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 4700dda967..06db45ab79 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1132,6 +1132,7 @@ public extension TelegramEngine { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction )) if let entry = CodableEntry(updatedItem) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 22af420350..0664a27b9c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -280,7 +280,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal S var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = [] for chat in chats { - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _) = chat { if let participantsCount = participantsCount { memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift index c7c1b2c786..bd32c6d735 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift @@ -31,7 +31,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel] var participantsCounts: [PeerId: Int32] = [:] for chat in chats { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _): if let participantsCountValue = participantsCountValue { participantsCounts[chat.peerId] = participantsCountValue } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index b1c40319bd..fb35eb5f59 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -38,7 +38,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 2d91867264..ebbcad7d1d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -820,7 +820,7 @@ public extension TelegramEngine { |> beforeNext { _ in let delayTime = CFAbsoluteTimeGetCurrent() - startTime if delayTime > 0.3 { - Logger.shared.log("getNextUnreadChannel", "took \(delayTime) s") + //Logger.shared.log("getNextUnreadChannel", "took \(delayTime) s") } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift index 1b8fc0a08b..28dccb7329 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -68,5 +68,21 @@ public extension TelegramEngine { public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal { return _internal_updateCloseFriends(account: self.account, peerIds: peerIds) } + + public func cleanupSessionReviews() -> Signal { + return _internal_cleanupSessionReviews(account: self.account) + } + + public func confirmNewSessionReview(id: Int64) -> Signal { + let _ = removeNewSessionReviews(postbox: self.account.postbox, ids: [id]).start() + return _internal_confirmNewSessionReview(account: self.account, id: id) + } + + public func terminateAnotherSession(id: Int64) -> Signal { + let _ = removeNewSessionReviews(postbox: self.account.postbox, ids: [id]).start() + + return terminateAccountSession(account: self.account, hash: id) + |> ignoreValues + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index 040c33c17c..49346c9a2e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -166,7 +166,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal = .single(ContextController.Items(content: .list(subItems))) let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceLabelView)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 33f4f50b75..3c4e7bd031 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -187,6 +187,7 @@ public final class StoryContentContextImpl: StoryContentContext { isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, isEdited: item.isEdited, + isMy: item.isMy, myReaction: item.myReaction ) } @@ -220,6 +221,7 @@ public final class StoryContentContextImpl: StoryContentContext { isSelectedContacts: item.privacy.base == .nobody, isForwardingDisabled: false, isEdited: false, + isMy: true, myReaction: nil )) totalCount += 1 @@ -1096,6 +1098,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isSelectedContacts: itemValue.isSelectedContacts, isForwardingDisabled: itemValue.isForwardingDisabled, isEdited: itemValue.isEdited, + isMy: itemValue.isMy, myReaction: itemValue.myReaction ) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 7552cea851..7d7c86c05c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5679,59 +5679,68 @@ public final class StoryItemSetContainerComponent: Component { guard let component = self.component, let controller = component.controller() else { return } + guard case let .channel(channel) = component.slice.peer else { + return + } self.dismissAllTooltips() let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - self.openStoryEditing() - }))) + if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.editStories) { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing() + }))) + } - items.append(.separator) + if !items.isEmpty { + items.append(.separator) + } - //TODO:localize - items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from Posts" : "Save to Posts", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.peer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start() - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + if channel.hasPermission(.editStories) { //TODO:localize - if component.slice.item.storyItem.isPinned { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: nil, text: "Story removed from the channel's profile", timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - blurred: true, - action: { _ in return false } - ), nil) - } else { - self.component?.presentController(UndoOverlayController( - presentationData: presentationData, - content: .info(title: "Story saved to the channel's profile", text: "Saved stories can be viewed by others on the channel's profile until an admin removes them.", timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - blurred: true, - action: { _ in return false } - ), nil) - } - }))) + items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from Posts" : "Save to Posts", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.peer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + //TODO:localize + if component.slice.item.storyItem.isPinned { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Story removed from the channel's profile", timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + blurred: true, + action: { _ in return false } + ), nil) + } else { + self.component?.presentController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: "Story saved to the channel's profile", text: "Saved stories can be viewed by others on the channel's profile until an admin removes them.", timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + blurred: true, + action: { _ in return false } + ), nil) + } + }))) + } let saveText: String = component.strings.Story_Context_SaveToGallery items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in @@ -5745,23 +5754,6 @@ public final class StoryItemSetContainerComponent: Component { self.requestSave() }))) - if case let .user(accountUser) = component.slice.peer { - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - if accountUser.isPremium { - self.sendMessageContext.requestStealthMode(view: self) - } else { - self.presentStealthModeUpgradeScreen() - } - }))) - } - if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) @@ -5773,7 +5765,7 @@ public final class StoryItemSetContainerComponent: Component { } let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) - |> deliverOnMainQueue).start(next: { [weak self] link in + |> deliverOnMainQueue).start(next: { [weak self] link in guard let self, let component = self.component else { return } @@ -5803,48 +5795,50 @@ public final class StoryItemSetContainerComponent: Component { }))) } - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) - let actionSheet = ActionSheetController(presentationData: presentationData) - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: component.strings.Story_ContextDeleteStory, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let component = self.component else { - return - } - component.delete() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - - actionSheet.dismissed = { [weak self] _ in - guard let self else { + if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.deleteStories) { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { return } - self.sendMessageContext.actionSheet = nil + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + let actionSheet = ActionSheetController(presentationData: presentationData) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: component.strings.Story_ContextDeleteStory, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let component = self.component else { + return + } + component.delete() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + + actionSheet.dismissed = { [weak self] _ in + guard let self else { + return + } + self.sendMessageContext.actionSheet = nil + self.updateIsProgressPaused() + } + self.sendMessageContext.actionSheet = actionSheet self.updateIsProgressPaused() - } - self.sendMessageContext.actionSheet = actionSheet - self.updateIsProgressPaused() - - component.presentController(actionSheet, nil) - }))) + + component.presentController(actionSheet, nil) + }))) + } let (tip, tipSignal) = self.getLinkedStickerPacks() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 459a74eb6a..ef499922c8 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -5321,10 +5321,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } else if let channel = peer as? TelegramChannel { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData { - if channel.hasPermission(.sendSomething) { + if channel.hasPermission(.editStories) { //TODO:localize items.append(.action(ContextMenuActionItem(text: "Archived Stories", icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent)