import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import TelegramNotices import ContactsPeerItem import ContextUI import ItemListUI import SearchUI import ChatListSearchItemHeader import PremiumUI import AnimationCache import MultiAnimationRenderer import Postbox import ChatFolderLinkPreviewScreen import StoryContainerScreen import ChatListHeaderComponent import UndoUI import NewSessionInfoScreen public enum ChatListNodeMode { case chatList(appendContacts: Bool) case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool) } struct ChatListNodeListViewTransition { let chatListView: ChatListNodeView let deleteItems: [ListViewDeleteItem] let insertItems: [ListViewInsertItem] let updateItems: [ListViewUpdateItem] let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? let adjustScrollToFirstItem: Bool let animateCrossfade: Bool } final class ChatListHighlightedLocation: Equatable { let location: ChatLocation let progress: CGFloat init(location: ChatLocation, progress: CGFloat) { self.location = location self.progress = progress } func withUpdatedProgress(_ progress: CGFloat) -> ChatListHighlightedLocation { return ChatListHighlightedLocation(location: location, progress: progress) } static func ==(lhs: ChatListHighlightedLocation, rhs: ChatListHighlightedLocation) -> Bool { if lhs.location != rhs.location { return false } if lhs.progress != rhs.progress { return false } return true } } public final class ChatListNodeInteraction { public enum PeerEntry { case peerId(EnginePeer.Id) case peer(EnginePeer) } let activateSearch: () -> Void let peerSelected: (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?) -> Void let disabledPeerSelected: (EnginePeer, Int64?) -> Void let togglePeerSelected: (EnginePeer, Int64?) -> Void let togglePeersSelection: ([PeerEntry], Bool) -> Void let additionalCategorySelected: (Int) -> Void let messageSelected: (EnginePeer, Int64?, EngineMessage, ChatListNodeEntryPromoInfo?) -> Void let groupSelected: (EngineChatList.Group) -> Void let addContact: (String) -> Void let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void let setItemPinned: (EngineChatList.PinnedItem.Id, Bool) -> Void let setPeerMuted: (EnginePeer.Id, Bool) -> Void let setPeerThreadMuted: (EnginePeer.Id, Int64?, Bool) -> Void let deletePeer: (EnginePeer.Id, Bool) -> Void let deletePeerThread: (EnginePeer.Id, Int64) -> Void let setPeerThreadStopped: (EnginePeer.Id, Int64, Bool) -> Void let setPeerThreadPinned: (EnginePeer.Id, Int64, Bool) -> Void let setPeerThreadHidden: (EnginePeer.Id, Int64, Bool) -> Void let updatePeerGrouping: (EnginePeer.Id, Bool) -> Void let togglePeerMarkedUnread: (EnginePeer.Id, Bool) -> Void let toggleArchivedFolderHiddenByDefault: () -> Void let toggleThreadsSelection: ([Int64], Bool) -> Void let hidePsa: (EnginePeer.Id) -> Void let activateChatPreview: (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void let present: (ViewController) -> Void let openForumThread: (EnginePeer.Id, Int64) -> Void let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void let openActiveSessions: () -> Void let performActiveSessionAction: (NewSessionReview, Bool) -> Void let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? var isSearchMode: Bool = false var isInlineMode: Bool = false var inlineNavigationLocation: ChatListHighlightedLocation? let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer public init( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, activateSearch: @escaping () -> Void, peerSelected: @escaping (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?) -> Void, togglePeerSelected: @escaping (EnginePeer, Int64?) -> Void, togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (EnginePeer, Int64?, EngineMessage, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (EngineChatList.Group) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, setItemPinned: @escaping (EngineChatList.PinnedItem.Id, Bool) -> Void, setPeerMuted: @escaping (EnginePeer.Id, Bool) -> Void, setPeerThreadMuted: @escaping (EnginePeer.Id, Int64?, Bool) -> Void, deletePeer: @escaping (EnginePeer.Id, Bool) -> Void, deletePeerThread: @escaping (EnginePeer.Id, Int64) -> Void, setPeerThreadStopped: @escaping (EnginePeer.Id, Int64, Bool) -> Void, setPeerThreadPinned: @escaping (EnginePeer.Id, Int64, Bool) -> Void, setPeerThreadHidden: @escaping (EnginePeer.Id, Int64, Bool) -> Void, updatePeerGrouping: @escaping (EnginePeer.Id, Bool) -> Void, togglePeerMarkedUnread: @escaping (EnginePeer.Id, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, toggleThreadsSelection: @escaping ([Int64], Bool) -> Void, hidePsa: @escaping (EnginePeer.Id) -> Void, activateChatPreview: @escaping (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, present: @escaping (ViewController) -> Void, openForumThread: @escaping (EnginePeer.Id, Int64) -> Void, openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, openActiveSessions: @escaping () -> Void, performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void, openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected self.togglePeerSelected = togglePeerSelected self.togglePeersSelection = togglePeersSelection self.additionalCategorySelected = additionalCategorySelected self.messageSelected = messageSelected self.groupSelected = groupSelected self.addContact = addContact self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.setItemPinned = setItemPinned self.setPeerMuted = setPeerMuted self.setPeerThreadMuted = setPeerThreadMuted self.deletePeer = deletePeer self.deletePeerThread = deletePeerThread self.setPeerThreadStopped = setPeerThreadStopped self.setPeerThreadPinned = setPeerThreadPinned self.setPeerThreadHidden = setPeerThreadHidden self.updatePeerGrouping = updatePeerGrouping self.togglePeerMarkedUnread = togglePeerMarkedUnread self.toggleArchivedFolderHiddenByDefault = toggleArchivedFolderHiddenByDefault self.toggleThreadsSelection = toggleThreadsSelection self.hidePsa = hidePsa self.activateChatPreview = activateChatPreview self.present = present self.animationCache = animationCache self.animationRenderer = animationRenderer self.openForumThread = openForumThread self.openStorageManagement = openStorageManagement self.openPasswordSetup = openPasswordSetup self.openPremiumIntro = openPremiumIntro self.openActiveSessions = openActiveSessions self.performActiveSessionAction = performActiveSessionAction self.openChatFolderUpdates = openChatFolderUpdates self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories } } public final class ChatListNodePeerInputActivities { public struct ItemId: Hashable { public var peerId: EnginePeer.Id public var threadId: Int64? public init(peerId: EnginePeer.Id, threadId: Int64?) { self.peerId = peerId self.threadId = threadId } } public let activities: [ItemId: [(EnginePeer, PeerInputActivity)]] public init(activities: [ItemId: [(EnginePeer, PeerInputActivity)]]) { self.activities = activities } } private func areFoundPeerArraysEqual(_ lhs: [(EnginePeer, EnginePeer?)], _ rhs: [(EnginePeer, EnginePeer?)]) -> Bool { if lhs.count != rhs.count { return false } for i in 0 ..< lhs.count { if lhs[i].0 != rhs[i].0 || lhs[i].1 != rhs[i].1 { return false } } return true } public struct ChatListNodeState: Equatable { public struct StoryState: Equatable { public var stats: EngineChatList.StoryStats public var hasUnseenCloseFriends: Bool public init(stats: EngineChatList.StoryStats, hasUnseenCloseFriends: Bool) { self.stats = stats self.hasUnseenCloseFriends = hasUnseenCloseFriends } } public struct ItemId: Hashable { public var peerId: EnginePeer.Id public var threadId: Int64? public init(peerId: EnginePeer.Id, threadId: Int64?) { self.peerId = peerId self.threadId = threadId } } public var presentationData: ChatListPresentationData public var editing: Bool public var peerIdWithRevealedOptions: ItemId? public var selectedPeerIds: Set public var peerInputActivities: ChatListNodePeerInputActivities? public var pendingRemovalItemIds: Set public var pendingClearHistoryPeerIds: Set public var hiddenItemShouldBeTemporaryRevealed: Bool public var selectedAdditionalCategoryIds: Set public var hiddenPsaPeerId: EnginePeer.Id? public var foundPeers: [(EnginePeer, EnginePeer?)] public var selectedPeerMap: [EnginePeer.Id: EnginePeer] public var selectedThreadIds: Set public var archiveStoryState: StoryState? public init( presentationData: ChatListPresentationData, editing: Bool, peerIdWithRevealedOptions: ItemId?, selectedPeerIds: Set, foundPeers: [(EnginePeer, EnginePeer?)], selectedPeerMap: [EnginePeer.Id: EnginePeer], selectedAdditionalCategoryIds: Set, peerInputActivities: ChatListNodePeerInputActivities?, pendingRemovalItemIds: Set, pendingClearHistoryPeerIds: Set, hiddenItemShouldBeTemporaryRevealed: Bool, hiddenPsaPeerId: EnginePeer.Id?, selectedThreadIds: Set, archiveStoryState: StoryState? ) { self.presentationData = presentationData self.editing = editing self.peerIdWithRevealedOptions = peerIdWithRevealedOptions self.selectedPeerIds = selectedPeerIds self.selectedAdditionalCategoryIds = selectedAdditionalCategoryIds self.foundPeers = foundPeers self.selectedPeerMap = selectedPeerMap self.peerInputActivities = peerInputActivities self.pendingRemovalItemIds = pendingRemovalItemIds self.pendingClearHistoryPeerIds = pendingClearHistoryPeerIds self.hiddenItemShouldBeTemporaryRevealed = hiddenItemShouldBeTemporaryRevealed self.hiddenPsaPeerId = hiddenPsaPeerId self.selectedThreadIds = selectedThreadIds self.archiveStoryState = archiveStoryState } public static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { if lhs.presentationData !== rhs.presentationData { return false } if lhs.editing != rhs.editing { return false } if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions { return false } if lhs.selectedPeerIds != rhs.selectedPeerIds { return false } if areFoundPeerArraysEqual(lhs.foundPeers, rhs.foundPeers) { return false } if lhs.selectedPeerMap != rhs.selectedPeerMap { return false } if lhs.selectedAdditionalCategoryIds != rhs.selectedAdditionalCategoryIds { return false } if lhs.peerInputActivities !== rhs.peerInputActivities { return false } if lhs.pendingRemovalItemIds != rhs.pendingRemovalItemIds { return false } if lhs.pendingClearHistoryPeerIds != rhs.pendingClearHistoryPeerIds { return false } if lhs.hiddenItemShouldBeTemporaryRevealed != rhs.hiddenItemShouldBeTemporaryRevealed { return false } if lhs.hiddenPsaPeerId != rhs.hiddenPsaPeerId { return false } if lhs.selectedThreadIds != rhs.selectedThreadIds { return false } if lhs.archiveStoryState != rhs.archiveStoryState { return false } return true } } private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case .HeaderEntry: return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): var header: ChatListSearchItemHeader? if case .peerType = mode { } else { if case .action = appearance { // TODO: hack, generalize header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), context: context, title: title, image: image, appearance: appearance, isSelected: selected, header: header, action: { nodeInteraction.additionalCategorySelected(id) } ), directionHint: entry.directionHint) case let .PeerEntry(peerEntry): let index = peerEntry.index let presentationData = peerEntry.presentationData let combinedReadState = peerEntry.readState let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount let draftState = peerEntry.draftState let peer = peerEntry.peer let threadInfo = peerEntry.threadInfo let presence = peerEntry.presence let hasUnseenMentions = peerEntry.hasUnseenMentions let hasUnseenReactions = peerEntry.hasUnseenReactions let editing = peerEntry.editing let hasActiveRevealControls = peerEntry.hasActiveRevealControls let selected = peerEntry.selected let inputActivities = peerEntry.inputActivities let promoInfo = peerEntry.promoInfo let hasFailedMessages = peerEntry.hasFailedMessages let isContact = peerEntry.isContact let forumTopicData = peerEntry.forumTopicData let topForumTopicItems = peerEntry.topForumTopicItems let revealed = peerEntry.revealed switch mode { case .chatList: return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( presentationData: presentationData, context: context, chatListLocation: location, filterData: filterData, index: index, content: .peer(ChatListItemContent.PeerData( messages: peerEntry.messages, peer: peer, threadInfo: threadInfo, combinedReadState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, presence: presence, hasUnseenMentions: hasUnseenMentions, hasUnseenReactions: hasUnseenReactions, draftState: draftState, inputActivities: inputActivities, promoInfo: promoInfo, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages, forumTopicData: forumTopicData, topForumTopicItems: topForumTopicItems, autoremoveTimeout: peerEntry.autoremoveTimeout, storyState: peerEntry.storyState.flatMap { storyState in return ChatListItemContent.StoryState( stats: storyState.stats, hasUnseenCloseFriends: storyState.hasUnseenCloseFriends ) } )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: threadInfo?.isHidden == true && !revealed, interaction: nodeInteraction ), directionHint: entry.directionHint) case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): let itemPeer = peer.chatMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } var enabled = true if let isPeerEnabled { if let itemPeer { enabled = isPeerEnabled(itemPeer) } } else { if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer._asPeer()) { enabled = false } } else { enabled = false } if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer { if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) { } else { enabled = false } } } if filter.contains(.onlyPrivateChats) { if let peer = peer.peers[peer.peerId] { switch peer { case .user, .secretChat: break default: enabled = false } } else { enabled = false } } if filter.contains(.onlyGroups) { if let peer = peer.peers[peer.peerId] { if case .legacyGroup = peer { } else if case let .channel(peer) = peer, case .group = peer.info { } else { enabled = false } } else { enabled = false } } if filter.contains(.onlyManageable) { if let peer = peer.peers[peer.peerId] { var canManage = false if case let .legacyGroup(peer) = peer { switch peer.role { case .creator, .admin: canManage = true default: break } } if canManage { } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { } else { enabled = false } } else { enabled = false } } if filter.contains(.excludeChannels) { if let peer = peer.peers[peer.peerId] { if case let .channel(peer) = peer, case .broadcast = peer.info { enabled = false } } } } var header: ChatListSearchItemHeader? switch mode { case let .peers(_, _, additionalCategories, _, _, _): if !additionalCategories.isEmpty { let headerType: ChatListSearchItemHeaderType if case .action = additionalCategories[0].appearance { // TODO: hack, generalize headerType = .orImportIntoAnExistingGroup } else { headerType = .chats } header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } default: break } var status: ContactsPeerItemStatus = .none if isSelecting, let itemPeer = itemPeer { if displayPresence, let presence = presence { status = .presence(presence, presentationData.dateTimeFormat) } else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) { status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon) } else { status = .none } } let peerContent: ContactsPeerItemPeer if let threadInfo = threadInfo, let itemPeer = itemPeer { peerContent = .thread(peer: itemPeer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor) } else { peerContent = .peer(peer: itemPeer, chatPeer: chatPeer) } var threadId: Int64? switch index { case let .forum(_, _, threadIdValue, _, _): threadId = threadIdValue case .chatList: break } var isForum = false if let peer = chatPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) { isForum = true if editing, case .chatList = mode { enabled = false } } var selectable = editing if case .chatList = mode { if isForum { selectable = false } } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: peerContent, status: status, enabled: enabled, selection: selectable ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in if let chatPeer = chatPeer { if editing { nodeInteraction.togglePeerSelected(chatPeer, threadId) } else { nodeInteraction.peerSelected(chatPeer, nil, threadId, nil) } } }, disabledAction: isForum && editing ? nil : { _ in if let chatPeer = chatPeer { nodeInteraction.disabledPeerSelected(chatPeer, threadId) } }, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) case .peerType: let itemPeer = peer.chatMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) let status: ContactsPeerItemStatus = .none return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: peerContent, status: status, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer, nil, nil, nil) } }, disabledAction: nil, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(groupReferenceEntry): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( presentationData: groupReferenceEntry.presentationData, context: context, chatListLocation: location, filterData: filterData, index: groupReferenceEntry.index, content: .groupReference(ChatListItemContent.GroupReferenceData( groupId: groupReferenceEntry.groupId, peers: groupReferenceEntry.peers, message: groupReferenceEntry.message, unreadCount: groupReferenceEntry.unreadCount, hiddenByDefault: groupReferenceEntry.hiddenByDefault, storyState: groupReferenceEntry.storyState.flatMap { storyState in return ChatListItemContent.StoryState( stats: storyState.stats, hasUnseenCloseFriends: storyState.hasUnseenCloseFriends ) } )), editing: groupReferenceEntry.editing, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: true, hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, interaction: nodeInteraction ), directionHint: entry.directionHint) case let .ContactEntry(contactEntry): let header: ChatListSearchItemHeader? = nil var status: ContactsPeerItemStatus = .none status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat) let presentationData = contactEntry.presentationData let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: peerContent, status: status, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil) }, disabledAction: nil, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) case let .ArchiveIntro(presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) case let .EmptyIntro(presentationData): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) case let .SectionHeader(presentationData, displayHide): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? { hideChatListContacts(context: context) } : nil), directionHint: entry.directionHint) case let .Notice(presentationData, notice): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in switch action { case .activate: switch notice { case .clearStorage: nodeInteraction?.openStorageManagement() case .setupPassword: nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .reviewLogin: break } case .hide: switch notice { default: break } case let .buttonChoice(isPositive): switch notice { case let .reviewLogin(newSessionReview): nodeInteraction?.performActiveSessionAction(newSessionReview, isPositive) default: break } } }), directionHint: entry.directionHint) } } } private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .PeerEntry(peerEntry): let index = peerEntry.index let presentationData = peerEntry.presentationData let combinedReadState = peerEntry.readState let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount let draftState = peerEntry.draftState let peer = peerEntry.peer let threadInfo = peerEntry.threadInfo let presence = peerEntry.presence let hasUnseenMentions = peerEntry.hasUnseenMentions let hasUnseenReactions = peerEntry.hasUnseenReactions let editing = peerEntry.editing let hasActiveRevealControls = peerEntry.hasActiveRevealControls let selected = peerEntry.selected let inputActivities = peerEntry.inputActivities let promoInfo = peerEntry.promoInfo let hasFailedMessages = peerEntry.hasFailedMessages let isContact = peerEntry.isContact let forumTopicData = peerEntry.forumTopicData let topForumTopicItems = peerEntry.topForumTopicItems let revealed = peerEntry.revealed switch mode { case .chatList: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( presentationData: presentationData, context: context, chatListLocation: location, filterData: filterData, index: index, content: .peer(ChatListItemContent.PeerData( messages: peerEntry.messages, peer: peer, threadInfo: threadInfo, combinedReadState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, presence: presence, hasUnseenMentions: hasUnseenMentions, hasUnseenReactions: hasUnseenReactions, draftState: draftState, inputActivities: inputActivities, promoInfo: promoInfo, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages, forumTopicData: forumTopicData, topForumTopicItems: topForumTopicItems, autoremoveTimeout: peerEntry.autoremoveTimeout, storyState: peerEntry.storyState.flatMap { storyState in return ChatListItemContent.StoryState( stats: storyState.stats, hasUnseenCloseFriends: storyState.hasUnseenCloseFriends ) } )), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: threadInfo?.isHidden == true && !revealed, interaction: nodeInteraction ), directionHint: entry.directionHint) case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): let itemPeer = peer.chatMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } var enabled = true if let isPeerEnabled { if let itemPeer { enabled = isPeerEnabled(itemPeer) } } else { if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer._asPeer()) { enabled = false } } else { enabled = false } if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer { if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) { } else { enabled = false } } } if filter.contains(.excludeChannels) { if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info { enabled = false } } } var header: ChatListSearchItemHeader? switch mode { case let .peers(_, _, additionalCategories, _, _, _): if !additionalCategories.isEmpty { let headerType: ChatListSearchItemHeaderType if case .action = additionalCategories[0].appearance { // TODO: hack, generalize headerType = .orImportIntoAnExistingGroup } else { headerType = .chats } header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } default: break } var status: ContactsPeerItemStatus = .none if isSelecting, let itemPeer = itemPeer { if displayPresence, let presence = presence { status = .presence(presence, presentationData.dateTimeFormat) } else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) { status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon) } else { status = .none } } let peerContent: ContactsPeerItemPeer if let threadInfo = threadInfo, let itemPeer = itemPeer { peerContent = .thread(peer: itemPeer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor) } else { peerContent = .peer(peer: itemPeer, chatPeer: chatPeer) } var threadId: Int64? switch index { case let .forum(_, _, threadIdValue, _, _): threadId = threadIdValue case .chatList: break } var isForum = false if let peer = chatPeer, case let .channel(channel) = peer, channel.flags.contains(.isForum) { isForum = true if editing, case .chatList = mode { enabled = false } } var selectable = editing if case .chatList = mode { if isForum { selectable = false } } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: peerContent, status: status, enabled: enabled, selection: selectable ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in if let chatPeer = chatPeer { if editing { nodeInteraction.togglePeerSelected(chatPeer, threadId) } else { nodeInteraction.peerSelected(chatPeer, nil, threadId, nil) } } }, disabledAction: isForum && editing ? nil : { _ in if let chatPeer = chatPeer { nodeInteraction.disabledPeerSelected(chatPeer, threadId) } }, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) case .peerType: let itemPeer = peer.chatMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) let status: ContactsPeerItemStatus = .none return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: peerContent, status: status, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer, nil, nil, nil) } }, disabledAction: nil, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) case let .GroupReferenceEntry(groupReferenceEntry): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( presentationData: groupReferenceEntry.presentationData, context: context, chatListLocation: location, filterData: filterData, index: groupReferenceEntry.index, content: .groupReference(ChatListItemContent.GroupReferenceData( groupId: groupReferenceEntry.groupId, peers: groupReferenceEntry.peers, message: groupReferenceEntry.message, unreadCount: groupReferenceEntry.unreadCount, hiddenByDefault: groupReferenceEntry.hiddenByDefault, storyState: groupReferenceEntry.storyState.flatMap { storyState in return ChatListItemContent.StoryState( stats: storyState.stats, hasUnseenCloseFriends: storyState.hasUnseenCloseFriends ) } )), editing: groupReferenceEntry.editing, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: true, hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, interaction: nodeInteraction ), directionHint: entry.directionHint) case let .ContactEntry(contactEntry): let header: ChatListSearchItemHeader? = nil var status: ContactsPeerItemStatus = .none status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat) let presentationData = contactEntry.presentationData let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .generalSearch, peer: peerContent, status: status, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil) }, disabledAction: nil, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) case let .ArchiveIntro(presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) case let .EmptyIntro(presentationData): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) case let .SectionHeader(presentationData, displayHide): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? { hideChatListContacts(context: context) } : nil), directionHint: entry.directionHint) case let .Notice(presentationData, notice): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in switch action { case .activate: switch notice { case .clearStorage: nodeInteraction?.openStorageManagement() case .setupPassword: nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .reviewLogin: break } case .hide: switch notice { default: break } case let .buttonChoice(isPositive): switch notice { case let .reviewLogin(newSessionReview): nodeInteraction?.performActiveSessionAction(newSessionReview, isPositive) default: break } } }), directionHint: entry.directionHint) case .HeaderEntry: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData): var header: ChatListSearchItemHeader? if case .peerType = mode { } else { if case .action = appearance { // TODO: hack, generalize header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder), context: context, title: title, image: image, appearance: appearance, isSelected: selected, header: header, action: { nodeInteraction.additionalCategorySelected(id) } ), directionHint: entry.directionHint) } } } private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) } private final class ChatListOpaqueTransactionState { let chatListView: ChatListNodeView init(chatListView: ChatListNodeView) { self.chatListView = chatListView } } public enum ChatListSelectionOption { case previous(unread: Bool) case next(unread: Bool) case peerId(EnginePeer.Id) case index(Int) } public enum ChatListGlobalScrollOption { case none case top case unread } public enum ChatListNodeScrollPosition { case top(adjustForTempInset: Bool) } public enum ChatListNodeEmptyState: Equatable { case notEmpty(containsChats: Bool, onlyArchive: Bool, onlyGeneralThread: Bool) case empty(isLoading: Bool, hasArchiveInfo: Bool) } public final class ChatListNode: ListView { public enum OpenStoriesSubject { case peer(EnginePeer.Id) case archive } private let fillPreloadItems: Bool private let context: AccountContext private let location: ChatListControllerLocation private let mode: ChatListNodeMode private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer private let _ready = ValuePromise() private var didSetReady = false public var ready: Signal { return _ready.get() } private let _contentsReady = ValuePromise() private var didSetContentsReady = false public var contentsReady: Signal { return _contentsReady.get() } public var peerSelected: ((EnginePeer, Int64?, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)? public var disabledPeerSelected: ((EnginePeer, Int64?) -> Void)? public var additionalCategorySelected: ((Int) -> Void)? public var groupSelected: ((EngineChatList.Group) -> Void)? public var addContact: ((String) -> Void)? public var activateSearch: (() -> Void)? public var deletePeerChat: ((EnginePeer.Id, Bool) -> Void)? public var deletePeerThread: ((EnginePeer.Id, Int64) -> Void)? public var setPeerThreadStopped: ((EnginePeer.Id, Int64, Bool) -> Void)? public var setPeerThreadPinned: ((EnginePeer.Id, Int64, Bool) -> Void)? public var setPeerThreadHidden: ((EnginePeer.Id, Int64, Bool) -> Void)? public var updatePeerGrouping: ((EnginePeer.Id, Bool) -> Void)? public var presentAlert: ((String) -> Void)? public var present: ((ViewController) -> Void)? public var push: ((ViewController) -> Void)? public var toggleArchivedFolderHiddenByDefault: (() -> Void)? public var hidePsa: ((EnginePeer.Id) -> Void)? public var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? public var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)? private var theme: PresentationTheme private let viewProcessingQueue = Queue() private var chatListView: ChatListNodeView? var entriesCount: Int { if let chatListView = self.chatListView { return chatListView.filteredEntries.count } else { return 0 } } private var interaction: ChatListNodeInteraction? private var dequeuedInitialTransitionOnLayout = false private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? public private(set) var currentState: ChatListNodeState private let statePromise: ValuePromise public var state: Signal { return self.statePromise.get() } private var currentLocation: ChatListNodeLocation? public private(set) var chatListFilter: ChatListFilter? { didSet { self.chatListFilterValue.set(.single(self.chatListFilter)) if self.chatListFilter != oldValue { self.setChatListLocation(.initial(count: 50, filter: self.chatListFilter)) } } } private let updatedFilterDisposable = MetaDisposable() private let chatListFilterValue = Promise() var chatListFilterSignal: Signal { return self.chatListFilterValue.get() } private var hasUpdatedAppliedChatListFilterValueOnce = false private var currentAppliedChatListFilterValue: ChatListFilter? private let appliedChatListFilterValue = Promise() var appliedChatListFilterSignal: Signal { return self.appliedChatListFilterValue.get() } private let chatListLocation = ValuePromise() private let chatListDisposable = MetaDisposable() private var activityStatusesDisposable: Disposable? private let scrollToTopOptionPromise = Promise(.none) public var scrollToTopOption: Signal { return self.scrollToTopOptionPromise.get() } private let scrolledAtTop = ValuePromise(true) private var scrolledAtTopValue: Bool = true { didSet { if self.scrolledAtTopValue != oldValue { self.scrolledAtTop.set(self.scrolledAtTopValue) } } } public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? public var didBeginInteractiveDragging: ((ListView) -> Void)? public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)? private var currentIsEmptyState: ChatListNodeEmptyState? public var canExpandHiddenItems: (() -> Bool)? public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? private let currentRemovingItemId = Atomic(value: nil) public func setCurrentRemovingItemId(_ itemId: ChatListNodeState.ItemId?) { let _ = self.currentRemovingItemId.swap(itemId) } private var hapticFeedback: HapticFeedback? let preloadItems = Promise<[ChatHistoryPreloadItem]>([]) var didBeginSelectingChats: (() -> Void)? public var selectionCountChanged: ((Int) -> Void)? var isSelectionGestureEnabled = true public var selectionLimit: Int32 = 100 public var reachedSelectionLimit: ((Int32) -> Void)? private var visibleTopInset: CGFloat? private var originalTopInset: CGFloat? public var passthroughPeerSelection = false let hideArhiveIntro = ValuePromise(false, ignoreRepeated: true) private let chatFolderUpdates = Promise() private var pollFilterUpdatesDisposable: Disposable? private var chatFilterUpdatesDisposable: Disposable? public var scrollHeightTopInset: CGFloat { didSet { self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset } } public var startedScrollingAtUpperBound: Bool = false private let autoSetReady: Bool public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool) { self.context = context self.location = location self.chatListFilter = chatListFilter self.chatListFilterValue.set(.single(chatListFilter)) self.fillPreloadItems = fillPreloadItems self.mode = mode self.animationCache = animationCache self.animationRenderer = animationRenderer self.autoSetReady = autoSetReady let isMainTab = chatListFilter == nil && location == .chatList(groupId: .root) var isSelecting = false if case .peers(_, true, _, _, _, _) = mode { isSelecting = true } self.currentState = ChatListNodeState(presentationData: ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations), editing: isSelecting, peerIdWithRevealedOptions: nil, selectedPeerIds: Set(), foundPeers: [], selectedPeerMap: [:], selectedAdditionalCategoryIds: Set(), peerInputActivities: nil, pendingRemovalItemIds: Set(), pendingClearHistoryPeerIds: Set(), hiddenItemShouldBeTemporaryRevealed: false, hiddenPsaPeerId: nil, selectedThreadIds: Set(), archiveStoryState: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) self.theme = theme self.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight super.init() if case .internal = context.sharedContext.applicationBindings.appBuildType { self.useMainQueueTransactions = true } self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.verticalScrollIndicatorFollowsOverscroll = true self.keepMinimalScrollHeightWithTopInset = self.scrollHeightTopInset let nodeInteraction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() } }, peerSelected: { [weak self] peer, _, threadId, promoInfo in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peer, threadId, true, true, promoInfo) } }, disabledPeerSelected: { [weak self] peer, threadId in if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected { disabledPeerSelected(peer, threadId) } }, togglePeerSelected: { [weak self] peer, _ in guard let strongSelf = self else { return } if case .peers = strongSelf.mode, strongSelf.passthroughPeerSelection { if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peer, nil, true, true, nil) } return } var didBeginSelecting = false var count = 0 strongSelf.updateState { [weak self] state in var state = state if state.selectedPeerIds.contains(peer.id) { state.selectedPeerIds.remove(peer.id) } else { if state.selectedPeerIds.count < strongSelf.selectionLimit { if state.selectedPeerIds.isEmpty { didBeginSelecting = true } state.selectedPeerIds.insert(peer.id) state.selectedPeerMap[peer.id] = peer } else { self?.reachedSelectionLimit?(Int32(state.selectedPeerIds.count)) } } count = state.selectedPeerIds.count return state } strongSelf.selectionCountChanged?(count) if didBeginSelecting { strongSelf.didBeginSelectingChats?() } }, togglePeersSelection: { [weak self] peers, selected in self?.updateState { state in var state = state if selected { for peerEntry in peers { switch peerEntry { case let .peer(peer): state.selectedPeerIds.insert(peer.id) state.selectedPeerMap[peer.id] = peer case let .peerId(peerId): state.selectedPeerIds.insert(peerId) } } } else { for peerEntry in peers { switch peerEntry { case let .peer(peer): state.selectedPeerIds.remove(peer.id) case let .peerId(peerId): state.selectedPeerIds.remove(peerId) } } } return state } if selected && !peers.isEmpty { self?.didBeginSelectingChats?() } }, additionalCategorySelected: { [weak self] id in self?.additionalCategorySelected?(id) }, messageSelected: { [weak self] peer, threadId, message, promoInfo in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { var activateInput = false for media in message.media { if let action = media as? TelegramMediaAction { switch action.action { case .peerJoined, .groupCreated, .channelMigratedFromGroup, .historyCleared: activateInput = true default: break } } } peerSelected(peer, threadId, true, activateInput, promoInfo) } }, groupSelected: { [weak self] groupId in if let strongSelf = self, let groupSelected = strongSelf.groupSelected { groupSelected(groupId) } }, addContact: { _ in }, setPeerIdWithRevealedOptions: { [weak self] peerId, fromPeerId in if let strongSelf = self { strongSelf.updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions?.peerId) || (peerId != nil && fromPeerId == nil) || (peerId == nil && fromPeerId == nil) { var state = state if let peerId = peerId { state.peerIdWithRevealedOptions = ChatListNodeState.ItemId(peerId: peerId, threadId: nil) } else { state.peerIdWithRevealedOptions = nil } return state } else { return state } } } }, setItemPinned: { [weak self] itemId, _ in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let strongSelf = self else { return } guard case let .chatList(groupId) = strongSelf.location else { return } let isPremium = peer?.isPremium ?? false let location: TogglePeerChatPinnedLocation if let chatListFilter = chatListFilter { location = .filter(chatListFilter.id) } else { location = .group(groupId._asGroup()) } let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: itemId) |> deliverOnMainQueue).start(next: { result in if let strongSelf = self { switch result { case .done: break case let .limitExceeded(count, _): if isPremium { if case .filter = location { let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { return true }) strongSelf.push?(controller) } else { let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { return true }) strongSelf.push?(controller) } } else { if case .filter = location { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) replaceImpl?(premiumScreen) return true }) strongSelf.push?(controller) replaceImpl = { [weak controller] c in controller?.replace(with: c) } } else { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats) replaceImpl?(premiumScreen) return true }) strongSelf.push?(controller) replaceImpl = { [weak controller] c in controller?.replace(with: c) } } } } } }) }) }, setPeerMuted: { [weak self] peerId, _ in guard let strongSelf = self else { return } strongSelf.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil) |> deliverOnMainQueue).start(completed: { self?.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } self?.setCurrentRemovingItemId(nil) }) }, setPeerThreadMuted: { [weak self] peerId, threadId, value in self?.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: threadId)) let _ = (context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: threadId, muteInterval: value ? Int32.max : 0) |> deliverOnMainQueue).start(completed: { self?.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } self?.setCurrentRemovingItemId(nil) }) }, deletePeer: { [weak self] peerId, joined in self?.deletePeerChat?(peerId, joined) }, deletePeerThread: { [weak self] peerId, threadId in self?.deletePeerThread?(peerId, threadId) }, setPeerThreadStopped: { [weak self] peerId, threadId, isStopped in self?.setPeerThreadStopped?(peerId, threadId, isStopped) }, setPeerThreadPinned: { [weak self] peerId, threadId, isPinned in self?.setPeerThreadPinned?(peerId, threadId, isPinned) }, setPeerThreadHidden: { [weak self] peerId, threadId, isHidden in self?.setPeerThreadHidden?(peerId, threadId, isHidden) }, updatePeerGrouping: { [weak self] peerId, group in self?.updatePeerGrouping?(peerId, group) }, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in guard let context = context else { return } self?.setCurrentRemovingItemId(ChatListNodeState.ItemId(peerId: peerId, threadId: nil)) let _ = (context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil) |> deliverOnMainQueue).start(completed: { self?.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } self?.setCurrentRemovingItemId(nil) }) }, toggleArchivedFolderHiddenByDefault: { [weak self] in self?.toggleArchivedFolderHiddenByDefault?() }, toggleThreadsSelection: { [weak self] threadIds, selected in self?.updateState { state in var state = state if selected { for threadId in threadIds { state.selectedThreadIds.insert(threadId) } } else { for threadId in threadIds { state.selectedThreadIds.remove(threadId) } } return state } if selected && !threadIds.isEmpty { self?.didBeginSelectingChats?() } }, hidePsa: { [weak self] id in self?.hidePsa?(id) }, activateChatPreview: { [weak self] item, threadId, node, gesture, location in guard let strongSelf = self else { return } if let activateChatPreview = strongSelf.activateChatPreview { activateChatPreview(item, threadId, node, gesture, location) } else { gesture?.cancel() } }, present: { [weak self] c in self?.present?(c) }, openForumThread: { [weak self] peerId, threadId in guard let self else { return } let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, let peer else { return } self.peerSelected?(peer, threadId, true, true, nil) }) }, openStorageManagement: { [weak self] in guard let self else { return } let controller = self.context.sharedContext.makeStorageManagementController(context: self.context) self.push?(controller) }, openPasswordSetup: { [weak self] in guard let self else { return } Queue.mainQueue().after(0.6) { [weak self] in if let self { let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).start() } } let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) self.push?(controller) }, openPremiumIntro: { [weak self] in guard let self else { return } Queue.mainQueue().after(0.6) { [weak self] in if let self { let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .annualPremium).start() let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .upgradePremium).start() let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .restorePremium).start() } } let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) self.push?(controller) }, openActiveSessions: { [weak self] in guard let self else { return } let activeSessionsContext = self.context.engine.privacy.activeSessions() let _ = (activeSessionsContext.state |> filter { state in return !state.sessions.isEmpty } |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return } let recentSessionsController = self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext) self.push?(recentSessionsController) }) }, performActiveSessionAction: { [weak self] newSessionReview, isPositive in guard let self else { return } if isPositive { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let animationBackgroundColor: UIColor if presentationData.theme.overallDarkAppearance { animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor } else { animationBackgroundColor = UIColor(rgb: 0x474747) } self.present?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.ChatList_SessionReview_ConfirmToastTitle, text: presentationData.strings.ChatList_SessionReview_ConfirmToastText, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { [weak self] action in switch action { case .info: self?.interaction?.openActiveSessions() default: break } return true })) let _ = self.context.engine.privacy.confirmNewSessionReview(id: newSessionReview.id).start() } else { self.push?(NewSessionInfoScreen(context: self.context, newSessionReview: newSessionReview)) //#if DEBUG //#else let _ = self.context.engine.privacy.terminateAnotherSession(id: newSessionReview.id).start() //#endif } }, openChatFolderUpdates: { [weak self] in guard let self else { return } let _ = (self.chatFolderUpdates.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self, let result else { return } self.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(result), contents: result.chatFolderLinkContents)) }) }, hideChatFolderUpdates: { [weak self] in guard let self else { return } let _ = (self.chatFolderUpdates.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self, let result else { return } if let localFilterId = result.chatFolderLinkContents.localFilterId { let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start() } }) }, openStories: { [weak self] subject, itemNode in guard let self else { return } self.openStories?(subject, itemNode) }) nodeInteraction.isInlineMode = isInlineMode let viewProcessingQueue = self.viewProcessingQueue let chatListViewUpdate = self.chatListLocation.get() |> distinctUntilChanged |> mapToSignal { listLocation -> Signal<(ChatListNodeViewUpdate, ChatListFilter?), NoError> in return chatListViewForLocation(chatListLocation: location, location: listLocation, account: context.account) |> map { update in return (update, listLocation.filter) } } let previousState = Atomic(value: self.currentState) let previousView = Atomic(value: nil) let previousHideArchivedFolderByDefault = Atomic(value: nil) let currentRemovingItemId = self.currentRemovingItemId let savedMessagesPeer: Signal if case let .peers(filter, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil { savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> map(Optional.init) |> map { peer in return peer.flatMap(EnginePeer.init) } } else { savedMessagesPeer = .single(nil) } let hideArchivedFolderByDefault = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatArchiveSettings]) |> map { view -> Bool in let settings: ChatArchiveSettings = view.values[ApplicationSpecificPreferencesKeys.chatArchiveSettings]?.get(ChatArchiveSettings.self) ?? .default return settings.isHiddenByDefault } |> distinctUntilChanged let displayArchiveIntro: Signal if case .chatList(.archive) = location { let displayArchiveIntroData = context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.archiveIntroDismissedKey()) |> map { entry -> Bool in if let value = entry.value?.get(ApplicationSpecificVariantNotice.self) { return !value.value } else { return true } } |> take(1) |> afterNext { value in Queue.mainQueue().async { if value { let _ = (context.sharedContext.accountManager.transaction { transaction -> Void in ApplicationSpecificNotice.setArchiveIntroDismissed(transaction: transaction, value: true) }).start() } } } displayArchiveIntro = combineLatest(displayArchiveIntroData, self.hideArhiveIntro.get()) |> map { a, b -> Bool in return a && !b } } else { displayArchiveIntro = .single(false) } let suggestedChatListNotice: Signal if case .chatList(groupId: .root) = location, chatListFilter == nil { let _ = context.engine.privacy.cleanupSessionReviews().start() suggestedChatListNotice = .single(nil) |> then ( combineLatest( getServerProvidedSuggestions(account: context.account), context.engine.auth.twoStepVerificationConfiguration(), newSessionReviews(postbox: context.account.postbox) ) |> mapToSignal { suggestions, configuration, newSessionReviews -> Signal in if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview)) } if suggestions.contains(.setupPassword) { var notSet = false switch configuration { case let .notSet(pendingEmail): if pendingEmail == nil { notSet = true } case .set: break } if notSet { return .single(.setupPassword) } } if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { return inAppPurchaseManager.availableProducts |> map { products -> ChatListNotice? in if products.count > 1 { let shortestOptionPrice: (Int64, NSDecimalNumber) if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) { shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) } else { shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) } for product in products { if product.id.hasSuffix(".annual") { let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0) let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0) if discount > 0 { if suggestions.contains(.restorePremium) { return .premiumRestore(discount: discount) } else if suggestions.contains(.annualPremium) { return .premiumAnnualDiscount(discount: discount) } else if suggestions.contains(.upgradePremium) { return .premiumUpgrade(discount: discount) } } break } } return nil } else { return nil } } } else { return .single(nil) } } ) |> distinctUntilChanged } else { suggestedChatListNotice = .single(nil) } let storageInfo: Signal if !"".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil { let totalSizeSignal = combineLatest(context.account.postbox.mediaBox.storageBox.totalSize(), context.account.postbox.mediaBox.cacheStorageBox.totalSize()) |> map { a, b -> Int64 in return a + b } storageInfo = totalSizeSignal |> take(1) |> mapToSignal { initialSize -> Signal in #if DEBUG let fractionLimit: Double = 0.0001 #else let fractionLimit: Double = 0.3 #endif let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0 let initialFraction: Double if deviceFreeSpace != 0 && initialSize != 0 { initialFraction = Double(initialSize) / Double(deviceFreeSpace + initialSize) } else { initialFraction = 0.0 } let initialReportSize: Double? if initialFraction > fractionLimit { initialReportSize = Double(initialSize) } else { initialReportSize = nil } final class ReportState { var lastSize: Int64 init(lastSize: Int64) { self.lastSize = lastSize } } let state = Atomic(value: ReportState(lastSize: initialSize)) let updatedReportSize: Signal = Signal { subscriber in let disposable = totalSizeSignal.start(next: { size in let updatedSize = state.with { state -> Int64 in if abs(initialSize - size) > 50 * 1024 * 1024 { state.lastSize = size return size } else { return -1 } } if updatedSize >= 0 { let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0 let updatedFraction: Double if deviceFreeSpace != 0 && updatedSize != 0 { updatedFraction = Double(updatedSize) / Double(deviceFreeSpace + updatedSize) } else { updatedFraction = 0.0 } let updatedReportSize: Double? if updatedFraction > fractionLimit { updatedReportSize = Double(updatedSize) } else { updatedReportSize = nil } subscriber.putNext(updatedReportSize) } }) return ActionDisposable { disposable.dispose() } } return .single(initialReportSize) |> then( updatedReportSize ) } } else { storageInfo = .single(nil) } let currentPeerId: EnginePeer.Id = context.account.peerId /*let contactList: Signal if case let .chatList(appendContacts) = mode, appendContacts { contactList = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)) |> map(Optional.init) } else { contactList = .single(nil) } let _ = contactList*/ /*let emptyInitialView = ChatListNodeView( originalList: EngineChatList( items: [], groupItems: [], additionalItems: [], hasEarlier: false, hasLater: false, isLoading: false ), filteredEntries: [ChatListNodeEntry.HeaderEntry], isLoading: false, filter: nil ) let _ = previousView.swap(emptyInitialView) let _ = (preparedChatListNodeViewTransition(from: nil, to: emptyInitialView, reason: .initial, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: nil, searchMode: false) |> map { mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: nil, mode: mode, isPeerEnabled: nil, transition: $0) }).start(next: { [weak self] value in guard let self else { return } let _ = self.enqueueTransition(value).start() })*/ let contacts: Signal<[ChatListContactPeer], NoError> if case .chatList(groupId: .root) = location, chatListFilter == nil, case .chatList = mode { contacts = ApplicationSpecificNotice.displayChatListContacts(accountManager: context.sharedContext.accountManager) |> distinctUntilChanged |> mapToSignal { value -> Signal<[ChatListContactPeer], NoError> in if value { return .single([]) } return context.engine.messages.chatList(group: .root, count: 10) |> map { chatList -> Bool in if chatList.items.count >= 5 { return true } else { return false } } |> distinctUntilChanged |> mapToSignal { hasChats -> Signal<[ChatListContactPeer], NoError> in if hasChats { return .single([]) } return context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) ) |> mapToThrottled { next -> Signal in return .single(next) |> then( .complete() |> delay(5.0, queue: Queue.concurrentDefaultQueue()) ) } |> map { contactList -> [ChatListContactPeer] in var result: [ChatListContactPeer] = [] for peer in contactList.peers { if peer.id == context.account.peerId { continue } result.append(ChatListContactPeer( peer: peer, presence: contactList.presences[peer.id] ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0) )) } result.sort(by: { lhs, rhs in if lhs.presence.status != rhs.presence.status { return lhs.presence.status < rhs.presence.status } else { return lhs.peer.id < rhs.peer.id } }) return result } } } } else { contacts = .single([]) } let accountPeerId = context.account.peerId let chatListNodeViewTransition = combineLatest( queue: viewProcessingQueue, hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, chatListViewUpdate, self.statePromise.get(), contacts ) |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) let notice: ChatListNotice? if let suggestedChatListNotice { notice = suggestedChatListNotice } else if let storageInfo { notice = .clearStorage(sizeFraction: storageInfo) } else { notice = nil } let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: isMainTab) var isEmpty = true var entries = rawEntries.filter { entry in switch entry { case let .PeerEntry(peerEntry): let peer = peerEntry.peer switch mode { case .chatList: isEmpty = false return true case let .peers(filter, _, _, _, _, _): guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false } guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false } guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false } if let peer = peer.peer { if peer.id.isReplies { return false } switch peer { case let .user(user): if user.botInfo != nil { if filter.contains(.excludeBots) { return false } } else { if filter.contains(.excludeUsers) { return false } } case .legacyGroup: if filter.contains(.excludeGroups) { return false } case let .channel(channel): switch channel.info { case .broadcast: if filter.contains(.excludeChannels) { return false } case .group: if filter.contains(.excludeGroups) { return false } } default: break } } if filter.contains(.onlyGroupsAndChannels) { if case .channel = peer.chatMainPeer { } else if case .legacyGroup = peer.chatMainPeer { } else { return false } } else { if filter.contains(.onlyGroups) { var isGroup: Bool = false if case let .channel(peer) = peer.chatMainPeer, case .group = peer.info { isGroup = true } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup { isGroup = true } if !isGroup { return false } } if filter.contains(.onlyChannels) { if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info { } else { return false } } } if filter.contains(.excludeChannels) { if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info { } } if filter.contains(.onlyWriteable) && filter.contains(.excludeDisabled) { if let peer = peer.peers[peer.peerId] { if !canSendMessagesToPeer(peer._asPeer()) { return false } } else { return false } } if filter.contains(.onlyManageable) && filter.contains(.excludeDisabled) { if let peer = peer.peers[peer.peerId] { var canManage = false if case let .legacyGroup(peer) = peer { switch peer.role { case .creator, .admin: canManage = true default: break } } if canManage { } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { } else { return false } } else { return false } } isEmpty = false return true case let .peerType(peerTypes, _): if let peer = peer.peer, !peer.isDeleted && peer.id != context.account.peerId { var match = false for peerType in peerTypes { if match { break } switch peerType { case let .user(userType): if case let .user(user) = peer { match = true if let isBot = userType.isBot { if isBot != (user.botInfo != nil) { match = false } } if let isPremium = userType.isPremium { if isPremium != user.isPremium { match = false } } isEmpty = false } else { match = false } case let .group(groupType): if case let .legacyGroup(group) = peer { match = true if groupType.isCreator { if case .creator = group.role { } else { match = false } } if let isForum = groupType.isForum, isForum { match = false } if let hasUsername = groupType.hasUsername, hasUsername { match = false } if let userAdminRights = groupType.userAdminRights { if case .creator = group.role, userAdminRights.rights.contains(.canBeAnonymous) { match = false } else if case let .admin(rights, _) = group.role { if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { match = false } } else if case .member = group.role { match = false } } isEmpty = false } else if case let .channel(channel) = peer, case .group = channel.info { match = true if groupType.isCreator { if !channel.flags.contains(.isCreator) { match = false } } if let isForum = groupType.isForum { if isForum != channel.flags.contains(.isForum) { match = false } } if let hasUsername = groupType.hasUsername { if hasUsername != (!(channel.addressName ?? "").isEmpty) { match = false } } if let userAdminRights = groupType.userAdminRights { if channel.flags.contains(.isCreator) { if let rights = channel.adminRights, rights.rights.contains(.canBeAnonymous) != userAdminRights.rights.contains(.canBeAnonymous) { match = false } } else if let rights = channel.adminRights { if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { match = false } } else { match = false } } isEmpty = false } else { match = false } case let .channel(channelType): if case let .channel(channel) = peer, case .broadcast = channel.info { match = true if channelType.isCreator { if !channel.flags.contains(.isCreator) { match = false } } if let hasUsername = channelType.hasUsername { if hasUsername != (!(channel.addressName ?? "").isEmpty) { match = false } } if let userAdminRights = channelType.userAdminRights { if channel.flags.contains(.isCreator) { if let rights = channel.adminRights, rights.rights.contains(.canBeAnonymous) != userAdminRights.rights.contains(.canBeAnonymous) { match = false } } else if let rights = channel.adminRights { if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { match = false } } else { match = false } } isEmpty = false } else { match = false } } if match { return true } } return false } else { return false } } case .ContactEntry: isEmpty = false return true case .GroupReferenceEntry: isEmpty = false return true default: return true } } if isEmpty { entries = [.HeaderEntry] } let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter) let previousView = previousView.swap(processedView) let previousState = previousState.swap(state) let reason: ChatListNodeViewTransitionReason var prepareOnMainQueue = false var previousWasEmptyOrSingleHole = false if let previous = previousView { if previous.filteredEntries.count == 1 { if case .HoleEntry = previous.filteredEntries[0] { previousWasEmptyOrSingleHole = true } else if case .HeaderEntry = previous.filteredEntries[0] { previousWasEmptyOrSingleHole = true } } else if previous.filteredEntries.isEmpty && previous.isLoading { previousWasEmptyOrSingleHole = true } } else { previousWasEmptyOrSingleHole = true } var updatedScrollPosition = update.scrollPosition if previousWasEmptyOrSingleHole { reason = .initial if previousView == nil { prepareOnMainQueue = true } } else { if previousView?.originalList === update.list { reason = .interactiveChanges updatedScrollPosition = nil } else { switch update.type { case .InitialUnread, .Initial: reason = .initial prepareOnMainQueue = true case .Generic: reason = .interactiveChanges case .UpdateVisible: reason = .reload case .FillHole: reason = .reload } } } let removingItemId = currentRemovingItemId.with { $0 } var disableAnimations = true if previousState.editing != state.editing { disableAnimations = false } else { var previousPinnedChats: [EnginePeer.Id] = [] var updatedPinnedChats: [EnginePeer.Id] = [] var previousPinnedThreads: [Int64] = [] var updatedPinnedThreads: [Int64] = [] var didIncludeRemovingPeerId = false var didIncludeHiddenByDefaultArchive = false var didIncludeHiddenThread = false var didIncludeNotice = false if let previous = previousView { for entry in previous.filteredEntries { if case let .PeerEntry(peerEntry) = entry { let index = peerEntry.index let threadInfo = peerEntry.threadInfo if let threadInfo, threadInfo.isHidden { didIncludeHiddenThread = true } if case let .chatList(chatListIndex) = index { if chatListIndex.pinningIndex != nil { previousPinnedChats.append(chatListIndex.messageIndex.id.peerId) } if ChatListNodeState.ItemId(peerId: chatListIndex.messageIndex.id.peerId, threadId: nil) == removingItemId { didIncludeRemovingPeerId = true } } else if case let .forum(pinnedIndex, _, threadId, _, _) = index { if case .index = pinnedIndex { previousPinnedThreads.append(threadId) } if case let .forum(peerId) = location, ChatListNodeState.ItemId(peerId: peerId, threadId: threadId) == removingItemId { didIncludeRemovingPeerId = true } } } else if case let .GroupReferenceEntry(groupReferenceEntry) = entry { didIncludeHiddenByDefaultArchive = groupReferenceEntry.hiddenByDefault } else if case .Notice = entry { didIncludeNotice = true } } } var doesIncludeRemovingPeerId = false var doesIncludeArchive = false var doesIncludeHiddenByDefaultArchive = false var doesIncludeNotice = false var doesIncludeHiddenThread = false for entry in processedView.filteredEntries { if case let .PeerEntry(peerEntry) = entry { let index = peerEntry.index let threadInfo = peerEntry.threadInfo if let threadInfo, threadInfo.isHidden { doesIncludeHiddenThread = true } if case let .chatList(index) = index, index.pinningIndex != nil { updatedPinnedChats.append(index.messageIndex.id.peerId) } else if case let .forum(pinnedIndex, _, threadId, _, _) = index { if case .index = pinnedIndex { updatedPinnedThreads.append(threadId) } } if case let .chatList(index) = index, ChatListNodeState.ItemId(peerId: index.messageIndex.id.peerId, threadId: nil) == removingItemId { doesIncludeRemovingPeerId = true } else if case let .forum(_, _, threadId, _, _) = index { if case let .forum(peerId) = location, ChatListNodeState.ItemId(peerId: peerId, threadId: threadId) == removingItemId { doesIncludeRemovingPeerId = true } } } else if case let .GroupReferenceEntry(groupReferenceEntry) = entry { doesIncludeArchive = true doesIncludeHiddenByDefaultArchive = groupReferenceEntry.hiddenByDefault } else if case .Notice = entry { doesIncludeNotice = true } } if previousPinnedChats != updatedPinnedChats || previousPinnedThreads != updatedPinnedThreads { disableAnimations = false } if previousState.selectedPeerIds != state.selectedPeerIds { disableAnimations = false } if previousState.selectedAdditionalCategoryIds != state.selectedAdditionalCategoryIds { disableAnimations = false } if doesIncludeRemovingPeerId != didIncludeRemovingPeerId { disableAnimations = false } if hideArchivedFolderByDefault && previousState.hiddenItemShouldBeTemporaryRevealed != state.hiddenItemShouldBeTemporaryRevealed && doesIncludeArchive { disableAnimations = false } if didIncludeHiddenByDefaultArchive != doesIncludeHiddenByDefaultArchive { disableAnimations = false } if previousState.hiddenItemShouldBeTemporaryRevealed != state.hiddenItemShouldBeTemporaryRevealed && doesIncludeHiddenThread { disableAnimations = false } if didIncludeHiddenThread != doesIncludeHiddenThread { disableAnimations = false } if didIncludeNotice != doesIncludeNotice { disableAnimations = false } } if let _ = previousHideArchivedFolderByDefaultValue, previousHideArchivedFolderByDefaultValue != hideArchivedFolderByDefault { disableAnimations = false } var searchMode = false if case .peers = mode { searchMode = true } if filter != previousView?.filter { disableAnimations = true updatedScrollPosition = nil } let filterData = filter.flatMap { filter -> ChatListItemFilterData? in if case let .filter(_, _, _, data) = filter { return ChatListItemFilterData(excludesArchived: data.excludeArchived) } else { return nil } } return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode) |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } let appliedTransition = chatListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in if let strongSelf = self { return strongSelf.enqueueTransition(transition) } return .complete() } self.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in if let strongSelf = self, let chatListView = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView { let originalList = chatListView.originalList if let range = range.loadedRange { var location: ChatListNodeLocation? if range.firstIndex < 5, let lastItem = originalList.items.last, originalList.hasLater { location = .navigation(index: lastItem.index, filter: strongSelf.chatListFilter) } else if range.firstIndex >= 5, range.lastIndex >= originalList.items.count - 5, originalList.hasEarlier, let firstItem = originalList.items.first { location = .navigation(index: firstItem.index, filter: strongSelf.chatListFilter) } if let location = location, location != strongSelf.currentLocation { strongSelf.setChatListLocation(location) } strongSelf.enqueueHistoryPreloadUpdate() } var refreshStoryPeerIds: [PeerId] = [] var isHiddenItemVisible = false if let range = range.visibleRange { let entryCount = chatListView.filteredEntries.count for i in max(0, range.firstIndex - 1) ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue } switch chatListView.filteredEntries[entryCount - i - 1] { case let .PeerEntry(peerEntry): let threadInfo = peerEntry.threadInfo if let threadInfo, threadInfo.isHidden { isHiddenItemVisible = true } if let peer = peerEntry.peer.chatMainPeer, !peerEntry.isContact, case let .user(user) = peer { refreshStoryPeerIds.append(user.id) } break case .GroupReferenceEntry: isHiddenItemVisible = true default: break } } } if !isHiddenItemVisible && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed { strongSelf.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false return state } } if !refreshStoryPeerIds.isEmpty { strongSelf.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: refreshStoryPeerIds) } } } self.interaction = nodeInteraction self.chatListDisposable.set(appliedTransition.start()) let initialLocation: ChatListNodeLocation switch mode { case .chatList: initialLocation = .initial(count: 50, filter: self.chatListFilter) case .peers, .peerType: initialLocation = .initial(count: 200, filter: self.chatListFilter) } self.setChatListLocation(initialLocation) let engine = context.engine let previousPeerCache = Atomic<[EnginePeer.Id: EnginePeer]>(value: [:]) let previousActivities = Atomic(value: nil) self.activityStatusesDisposable = (context.account.allPeerInputActivities() |> mapToSignal { activitiesByPeerId -> Signal<[ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]], NoError> in var activitiesByPeerId = activitiesByPeerId for key in activitiesByPeerId.keys { activitiesByPeerId[key]?.removeAll(where: { _, activity in switch activity { case .interactingWithEmoji: return true case .speakingInGroupCall: return true default: return false } }) } var foundAllPeers = true var cachedResult: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = [:] previousPeerCache.with { dict -> Void in for (chatPeerId, activities) in activitiesByPeerId { var threadId: Int64? switch location { case .chatList: guard case .global = chatPeerId.category else { continue } case let .forum(peerId): if chatPeerId.peerId != peerId { continue } guard case let .thread(threadIdValue) = chatPeerId.category else { continue } threadId = threadIdValue } var cachedChatResult: [(EnginePeer, PeerInputActivity)] = [] for (peerId, activity) in activities { if let peer = dict[peerId] { cachedChatResult.append((peer, activity)) } else { foundAllPeers = false break } cachedResult[ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: threadId)] = cachedChatResult } } } if foundAllPeers { return .single(cachedResult) } else { var dataKeys: [EnginePeer.Id] = [] for (peerId, activities) in activitiesByPeerId { dataKeys.append(peerId.peerId) for activity in activities { dataKeys.append(activity.0) } } return engine.data.get(EngineDataMap( Set(dataKeys).map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) } )) |> map { peerMap -> [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] in var result: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = [:] var peerCache: [EnginePeer.Id: EnginePeer] = [:] for (chatPeerId, activities) in activitiesByPeerId { let itemId: ChatListNodePeerInputActivities.ItemId switch location { case .chatList: guard case .global = chatPeerId.category else { continue } if case let .channel(channel) = peerMap[chatPeerId.peerId], channel.flags.contains(.isForum) { continue } itemId = ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: nil) case let .forum(peerId): if chatPeerId.peerId != peerId { continue } guard case let .thread(threadIdValue) = chatPeerId.category else { continue } itemId = ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: threadIdValue) } var chatResult: [(EnginePeer, PeerInputActivity)] = [] for (peerId, activity) in activities { if let maybePeer = peerMap[peerId], let peer = maybePeer { chatResult.append((peer, activity)) peerCache[peerId] = peer } } result[itemId] = chatResult } let _ = previousPeerCache.swap(peerCache) return result } } } |> map { activities -> ChatListNodePeerInputActivities? in return previousActivities.modify { current in var updated = false let currentList: [ChatListNodePeerInputActivities.ItemId: [(EnginePeer, PeerInputActivity)]] = current?.activities ?? [:] if currentList.count != activities.count { updated = true } else { outer: for (peerId, currentValue) in currentList { if let value = activities[peerId] { if currentValue.count != value.count { updated = true break outer } else { for i in 0 ..< currentValue.count { if currentValue[i].0 != value[i].0 { updated = true break outer } if currentValue[i].1 != value[i].1 { updated = true break outer } } } } else { updated = true break outer } } } if updated { if activities.isEmpty { return nil } else { return ChatListNodePeerInputActivities(activities: activities) } } else { return current } } } |> deliverOnMainQueue).start(next: { [weak self] activities in if let strongSelf = self { strongSelf.updateState { state in var state = state state.peerInputActivities = activities return state } } }) self.reorderItem = { [weak self] fromIndex, toIndex, transactionOpaqueState -> Signal in guard let strongSelf = self, let filteredEntries = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.filteredEntries else { return .single(false) } guard fromIndex >= 0 && fromIndex < filteredEntries.count && toIndex >= 0 && toIndex < filteredEntries.count else { return .single(false) } switch strongSelf.location { case let .chatList(groupId): let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] var referenceId: EngineChatList.PinnedItem.Id? var beforeAll = false switch toEntry { case let .PeerEntry(peerEntry): let index = peerEntry.index let promoInfo = peerEntry.promoInfo if promoInfo != nil { beforeAll = true } else { if case let .chatList(chatListIndex) = index { referenceId = .peer(chatListIndex.messageIndex.id.peerId) } } default: break } if case let .index(index) = fromEntry.sortIndex, case let .chatList(chatListIndex) = index, let _ = chatListIndex.pinningIndex { let location: TogglePeerChatPinnedLocation if let chatListFilter = chatListFilter { location = .filter(chatListFilter.id) } else { location = .group(groupId._asGroup()) } let engine = strongSelf.context.engine return engine.peers.getPinnedItemIds(location: location) |> mapToSignal { itemIds -> Signal in var itemIds = itemIds var itemId: EngineChatList.PinnedItem.Id? switch fromEntry { case let .PeerEntry(peerEntry): if case let .chatList(index) = peerEntry.index { itemId = .peer(index.messageIndex.id.peerId) } default: break } if let itemId = itemId { itemIds = itemIds.filter({ $0 != itemId }) if let referenceId = referenceId { var inserted = false for i in 0 ..< itemIds.count { if itemIds[i] == referenceId { if fromIndex < toIndex { itemIds.insert(itemId, at: i + 1) } else { itemIds.insert(itemId, at: i) } inserted = true break } } if !inserted { itemIds.append(itemId) } } else if beforeAll { itemIds.insert(itemId, at: 0) } else { itemIds.append(itemId) } return engine.peers.reorderPinnedItemIds(location: location, itemIds: itemIds) } else { return .single(false) } } } else { return .single(false) } case let .forum(peerId): let fromEntry = filteredEntries[filteredEntries.count - 1 - fromIndex] let toEntry = filteredEntries[filteredEntries.count - 1 - toIndex] var referenceId: Int64? var beforeAll = false switch toEntry { case let .PeerEntry(peerEntry): if peerEntry.promoInfo != nil { beforeAll = true } else { if case let .forum(_, _, threadId, _, _) = peerEntry.index { referenceId = threadId } } default: break } if case let .index(index) = fromEntry.sortIndex, case let .forum(pinningIndex, _, _, _, _) = index, case .index = pinningIndex { let engine = strongSelf.context.engine return engine.peers.getForumChannelPinnedTopics(id: peerId) |> mapToSignal { itemIds -> Signal in var itemIds = itemIds var itemId: Int64? switch fromEntry { case let .PeerEntry(peerEntry): if case let .forum(_, _, threadId, _, _) = peerEntry.index { itemId = threadId } default: break } if let itemId = itemId { itemIds = itemIds.filter({ $0 != itemId }) if let referenceId = referenceId { var inserted = false for i in 0 ..< itemIds.count { if itemIds[i] == referenceId { if fromIndex < toIndex { itemIds.insert(itemId, at: i + 1) } else { itemIds.insert(itemId, at: i) } inserted = true break } } if !inserted { itemIds.append(itemId) } } else if beforeAll { itemIds.insert(itemId, at: 0) } else { itemIds.append(itemId) } return engine.peers.setForumChannelPinnedTopics(id: peerId, threadIds: itemIds) |> map { _ -> Bool in } |> `catch` { _ -> Signal in return .single(false) } |> then(Signal.single(true)) } else { return .single(false) } } } else { return .single(false) } } } var startedScrollingWithCanExpandHiddenItems = false self.beganInteractiveDragging = { [weak self] _ in guard let strongSelf = self else { return } switch strongSelf.visibleContentOffset() { case .none, .unknown: strongSelf.startedScrollingAtUpperBound = false case let .known(value): strongSelf.startedScrollingAtUpperBound = value <= 0.001 } if let canExpandHiddenItems = strongSelf.canExpandHiddenItems { startedScrollingWithCanExpandHiddenItems = canExpandHiddenItems() } else { startedScrollingWithCanExpandHiddenItems = true } if strongSelf.currentState.peerIdWithRevealedOptions != nil { strongSelf.updateState { state in var state = state state.peerIdWithRevealedOptions = nil return state } } strongSelf.didBeginInteractiveDragging?(strongSelf) } self.didEndScrolling = { [weak self] _ in guard let strongSelf = self else { return } let _ = strongSelf.contentScrollingEnded?(strongSelf) let revealHiddenItems: Bool switch strongSelf.visibleContentOffset() { case .none, .unknown: revealHiddenItems = false case let .known(value): revealHiddenItems = value <= -strongSelf.tempTopInset - 60.0 } if !revealHiddenItems && strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed { /*strongSelf.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = false return state }*/ } } self.scrollToTopOptionPromise.set(combineLatest( renderedTotalUnreadCount(accountManager: self.context.sharedContext.accountManager, engine: self.context.engine) |> deliverOnMainQueue, self.scrolledAtTop.get() ) |> map { badge, scrolledAtTop -> ChatListGlobalScrollOption in if scrolledAtTop { return .none } else { return .top } }) self.visibleContentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return } if !strongSelf.dequeuedInitialTransitionOnLayout { return } let atTop: Bool var revealHiddenItems: Bool = false switch offset { case .none, .unknown: atTop = false case let .known(value): atTop = value <= -strongSelf.tempTopInset if strongSelf.startedScrollingAtUpperBound && startedScrollingWithCanExpandHiddenItems && strongSelf.isTracking { revealHiddenItems = value <= -strongSelf.tempTopInset - 60.0 } } strongSelf.scrolledAtTopValue = atTop strongSelf.contentOffsetChanged?(offset) if revealHiddenItems && !strongSelf.currentState.hiddenItemShouldBeTemporaryRevealed { //strongSelf.revealScrollHiddenItem() } } self.dynamicVisualInsets = { [weak self] in guard let self else { return UIEdgeInsets() } let _ = self return UIEdgeInsets() } self.pollFilterUpdates() self.resetFilter() let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) selectionRecognizer.shouldBegin = { [weak self] in guard let strongSelf = self else { return false } return strongSelf.isSelectionGestureEnabled } self.view.addGestureRecognizer(selectionRecognizer) } deinit { self.chatListDisposable.dispose() self.activityStatusesDisposable?.dispose() self.updatedFilterDisposable.dispose() self.pollFilterUpdatesDisposable?.dispose() self.chatFilterUpdatesDisposable?.dispose() } func updateFilter(_ filter: ChatListFilter?) { if filter?.id != self.chatListFilter?.id { self.chatListFilter = filter self.resetFilter() } } func hasItemsToBeRevealed() -> Bool { if self.currentState.hiddenItemShouldBeTemporaryRevealed { return false } var isHiddenItemVisible = false self.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { if case let .peer(peerData) = item.content, let threadInfo = peerData.threadInfo { if threadInfo.isHidden { isHiddenItemVisible = true } } if case let .groupReference(groupReference) = item.content { if groupReference.hiddenByDefault { isHiddenItemVisible = true } } } }) return isHiddenItemVisible } func revealScrollHiddenItem() { var isHiddenItemVisible = false self.forEachItemNode({ itemNode in if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { if case let .peer(peerData) = item.content, let threadInfo = peerData.threadInfo { if threadInfo.isHidden { isHiddenItemVisible = true } } if case let .groupReference(groupReference) = item.content { if groupReference.hiddenByDefault { isHiddenItemVisible = true } } } }) if isHiddenItemVisible && !self.currentState.hiddenItemShouldBeTemporaryRevealed { if self.hapticFeedback == nil { self.hapticFeedback = HapticFeedback() } self.hapticFeedback?.impact(.medium) self.updateState { state in var state = state state.hiddenItemShouldBeTemporaryRevealed = true return state } } } private func pollFilterUpdates() { self.chatFolderUpdates.set(.single(nil)) /*guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else { self.chatFolderUpdates.set(.single(nil)) return } self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start() self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } self.chatFolderUpdates.set(.single(result)) })*/ } private func resetFilter() { if let chatListFilter = self.chatListFilter { self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters() |> map { filters -> ChatListFilter? in for filter in filters { if filter.id == chatListFilter.id { return filter } } return nil } |> deliverOnMainQueue).start(next: { [weak self] updatedFilter in guard let strongSelf = self else { return } if strongSelf.chatListFilter != updatedFilter { strongSelf.chatListFilter = updatedFilter } })) } else { self.updatedFilterDisposable.set(nil) } } public func updateThemeAndStrings(theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) { if theme !== self.currentState.presentationData.theme || strings !== self.currentState.presentationData.strings || dateTimeFormat != self.currentState.presentationData.dateTimeFormat { self.theme = theme if self.keepTopItemOverscrollBackground != nil { self.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: theme.chatList.pinnedItemBackgroundColor, direction: true) } self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.updateState { state in var state = state state.presentationData = ChatListPresentationData(theme: theme, fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, disableAnimations: disableAnimations) return state } } } public func updateState(_ f: (ChatListNodeState) -> ChatListNodeState) { let state = f(self.currentState) if state != self.currentState { self.currentState = state self.statePromise.set(state) } } private func enqueueTransition(_ transition: ChatListNodeListViewTransition) -> Signal { return Signal { [weak self] subscriber in if let strongSelf = self { if let _ = strongSelf.enqueuedTransition { preconditionFailure() } strongSelf.enqueuedTransition = (transition, { subscriber.putCompletion() }) if strongSelf.isNodeLoaded, strongSelf.dequeuedInitialTransitionOnLayout { strongSelf.dequeueTransition() } else { if !strongSelf.didSetReady && strongSelf.autoSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } } } else { subscriber.putCompletion() } return EmptyDisposable } |> runOn(Queue.mainQueue()) } private func dequeueTransition() { if let (transition, completion) = self.enqueuedTransition { self.enqueuedTransition = nil let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in if let strongSelf = self { strongSelf.chatListView = transition.chatListView if strongSelf.fillPreloadItems { let filteredEntries = transition.chatListView.filteredEntries var preloadItems: [ChatHistoryPreloadItem] = [] if !transition.chatListView.originalList.hasLater { for entry in filteredEntries.reversed() { switch entry { case let .PeerEntry(peerEntry): if peerEntry.promoInfo == nil { var hasUnread = false if let combinedReadState = peerEntry.readState { hasUnread = combinedReadState.count > 0 } switch peerEntry.index { case let .chatList(index): preloadItems.append(ChatHistoryPreloadItem(index: index, threadId: nil, isMuted: peerEntry.isRemovedFromTotalUnreadCount, hasUnread: hasUnread)) case .forum: break } } default: break } if preloadItems.count >= 30 { break } } } strongSelf.preloadItems.set(.single(preloadItems)) } var pinnedOverscroll = false if case .chatList = strongSelf.mode { let entryCount = transition.chatListView.filteredEntries.count if entryCount >= 1 { if case let .index(index) = transition.chatListView.filteredEntries[entryCount - 1].sortIndex, case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex != nil { pinnedOverscroll = true } } } if pinnedOverscroll != (strongSelf.keepTopItemOverscrollBackground != nil) { if pinnedOverscroll { strongSelf.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: strongSelf.theme.chatList.pinnedItemBackgroundColor, direction: true) } else { strongSelf.keepTopItemOverscrollBackground = nil } } if let scrollToItem = transition.scrollToItem, case .center = scrollToItem.position { if let itemNode = strongSelf.itemNodeAtIndex(scrollToItem.index) as? ChatListItemNode { itemNode.flashHighlight() } } if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) } if !strongSelf.didSetContentsReady { strongSelf.didSetContentsReady = true strongSelf._contentsReady.set(true) } var isEmpty = false var isLoading = false var hasArchiveInfo = false if transition.chatListView.filteredEntries.isEmpty { isEmpty = true } else { if transition.chatListView.filteredEntries.count <= 2 { isEmpty = true loop1: for entry in transition.chatListView.filteredEntries { switch entry { case .HeaderEntry, .HoleEntry: break default: if case .ArchiveIntro = entry { hasArchiveInfo = true } isEmpty = false break loop1 } } isLoading = true var hasHoles = false loop2: for entry in transition.chatListView.filteredEntries { switch entry { case .HoleEntry: hasHoles = true case .HeaderEntry: break default: isLoading = false break loop2 } } if !hasHoles { isLoading = false } } else { for entry in transition.chatListView.filteredEntries.reversed().prefix(2) { if case .ArchiveIntro = entry { hasArchiveInfo = true break } } } } let isEmptyState: ChatListNodeEmptyState if transition.chatListView.isLoading { isEmptyState = .empty(isLoading: true, hasArchiveInfo: hasArchiveInfo) } else if isEmpty { isEmptyState = .empty(isLoading: isLoading, hasArchiveInfo: false) } else { var containsChats = false var threadCount = 0 var hasGeneral = false var hasArchive = false loop: for entry in transition.chatListView.filteredEntries { switch entry { case .GroupReferenceEntry, .HoleEntry, .PeerEntry, .ContactEntry: if case .GroupReferenceEntry = entry { hasArchive = true } else { containsChats = true } if case .forum = strongSelf.location { if case let .PeerEntry(peerEntry) = entry, let threadInfo = peerEntry.threadInfo { if threadInfo.id == 1 { hasGeneral = true } threadCount += 1 if threadCount > 1 { break loop } } } else { break loop } case .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .HeaderEntry, .AdditionalCategory: break } } isEmptyState = .notEmpty(containsChats: containsChats || hasArchive, onlyArchive: hasArchive && !containsChats, onlyGeneralThread: hasGeneral && threadCount == 1) } var insertedPeerIds: [EnginePeer.Id] = [] for item in transition.insertItems { if let item = item.item as? ChatListItem { switch item.content { case let .peer(peerData): insertedPeerIds.append(peerData.peer.peerId) case .groupReference: break } } } if !insertedPeerIds.isEmpty { strongSelf.addedVisibleChatsWithPeerIds?(insertedPeerIds) } var isEmptyUpdate: ContainedViewLayoutTransition = .immediate if transition.options.contains(.AnimateInsertion) || transition.animateCrossfade { isEmptyUpdate = .animated(duration: 0.25, curve: .easeInOut) } if strongSelf.currentIsEmptyState != isEmptyState { strongSelf.currentIsEmptyState = isEmptyState strongSelf.isEmptyUpdated?(isEmptyState, transition.chatListView.filter != nil, isEmptyUpdate) } if !strongSelf.hasUpdatedAppliedChatListFilterValueOnce || transition.chatListView.filter != strongSelf.currentAppliedChatListFilterValue { strongSelf.currentAppliedChatListFilterValue = transition.chatListView.filter strongSelf.appliedChatListFilterValue.set(.single(transition.chatListView.filter)) } completion() } } var options = transition.options //options.insert(.Synchronous) if self.view.window != nil { if !options.contains(.AnimateInsertion) { options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousResourceLoading) } if options.contains(.AnimateCrossfade) && !self.isDeceleratingAfterTracking { options.insert(.PreferSynchronousDrawing) } } var scrollToItem = transition.scrollToItem if transition.adjustScrollToFirstItem { var offset: CGFloat = 0.0 if let visibleTopInset = self.visibleTopInset { offset = visibleTopInset - self.insets.top } else { switch self.visibleContentOffset() { case let .known(value) where abs(value) < .ulpOfOne: offset = 0.0 default: offset = -self.scrollHeightTopInset } } scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } let updatedOpaqueState: Any? = ChatListOpaqueTransactionState(chatListView: transition.chatListView) self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: updatedOpaqueState, completion: completion) } } var isNavigationHidden: Bool { switch self.visibleContentOffset() { case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0: return false case .none: return false default: return true } } var isNavigationInAFinalState: Bool { switch self.visibleContentOffset() { case let .known(value): let _ = value /*if value < self.scrollHeightTopInset - 1.0 { if abs(value - 0.0) < 1.0 { return true } if abs(value - self.scrollHeightTopInset) < 1.0 { return true } return false } else {*/ return true //} default: return true } } func adjustScrollOffsetForNavigation(isNavigationHidden: Bool) { if self.isNavigationHidden == isNavigationHidden { return } var scrollToItem: ListViewScrollToItem? switch self.visibleContentOffset() { case let .known(value) where abs(value) < self.scrollHeightTopInset - 1.0: if isNavigationHidden { scrollToItem = ListViewScrollToItem(index: 0, position: .top(-self.scrollHeightTopInset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } default: if !isNavigationHidden { scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } } if let scrollToItem = scrollToItem { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } public func fixContentOffset(offset: CGFloat) { let _ = self.scrollToOffsetFromTop(offset, animated: false) /*let scrollToItem: ListViewScrollToItem = ListViewScrollToItem(index: 0, position: .top(-offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: scrollToItem, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })*/ } public var ignoreStoryInsetAdjustment: Bool = false private var previousStoriesInset: CGFloat? public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, visibleTopInset: CGFloat, originalTopInset: CGFloat, storiesInset: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat) { //print("inset: \(updateSizeAndInsets.insets.top)") var highlightedLocation: ChatListHighlightedLocation? if case let .forum(peerId) = inlineNavigationLocation { highlightedLocation = ChatListHighlightedLocation(location: .peer(id: peerId), progress: inlineNavigationTransitionFraction) } var navigationLocationPresenceUpdated = false if (self.interaction?.inlineNavigationLocation == nil) != (highlightedLocation == nil) { navigationLocationPresenceUpdated = true } var navigationLocationUpdated = false if self.interaction?.inlineNavigationLocation != highlightedLocation { self.interaction?.inlineNavigationLocation = highlightedLocation navigationLocationUpdated = true } let insetDelta: CGFloat = 0.0 if navigationLocationPresenceUpdated { let targetTopInset: CGFloat if highlightedLocation != nil { targetTopInset = self.visibleTopInset ?? self.insets.top } else { targetTopInset = self.originalTopInset ?? self.insets.top } let immediateInsetDelta = self.insets.top - targetTopInset self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, additionalScrollDistance: immediateInsetDelta, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: self.visibleSize, insets: UIEdgeInsets(top: targetTopInset, left: self.insets.left, bottom: self.insets.bottom, right: self.insets.right), duration: 0.0, curve: .Default(duration: 0.0)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } self.visualInsets = UIEdgeInsets(top: visibleTopInset, left: 0.0, bottom: 0.0, right: 0.0) self.visibleTopInset = visibleTopInset self.originalTopInset = originalTopInset var additionalScrollDistance: CGFloat = 0.0 if let previousStoriesInset = self.previousStoriesInset { if self.ignoreStoryInsetAdjustment { //additionalScrollDistance += -20.0 switch self.visibleContentOffset() { case let .known(value): additionalScrollDistance += min(0.0, value) default: break } additionalScrollDistance = 0.0 } else { additionalScrollDistance += previousStoriesInset - storiesInset } } self.previousStoriesInset = storiesInset //print("storiesInset: \(storiesInset), additionalScrollDistance: \(additionalScrollDistance)") var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency] if navigationLocationUpdated { options.insert(.ForceUpdate) if transition.isAnimated { options.insert(.AnimateInsertion) } additionalScrollDistance += insetDelta } self.ignoreStopScrolling = true self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: options, scrollToItem: nil, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.ignoreStopScrolling = false if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true self.dequeueTransition() } } public func scrollToPosition(_ position: ChatListNodeScrollPosition, animated: Bool = true) { var additionalDelta: CGFloat = 0.0 switch position { case let .top(adjustForTempInset): if adjustForTempInset { additionalDelta = ChatListNavigationBar.storiesScrollHeight self.tempTopInset = ChatListNavigationBar.storiesScrollHeight } } if let list = self.chatListView?.originalList { if !list.hasLater { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(additionalDelta), animated: animated, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(additionalDelta), animated: animated, filter: self.chatListFilter) self.setChatListLocation(location) } } else { let location: ChatListNodeLocation = .scroll(index: .chatList(.absoluteUpperBound), sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .top(additionalDelta), animated: animated, filter: self.chatListFilter) self.setChatListLocation(location) } } private func setChatListLocation(_ location: ChatListNodeLocation) { self.currentLocation = location self.chatListLocation.set(location) } private func relativeUnreadChatListIndex(position: EngineChatList.RelativePosition) -> Signal { guard case let .chatList(groupId) = self.location else { return .single(nil) } let engine = self.context.engine return self.context.sharedContext.accountManager.transaction { transaction -> Signal in var filter = true if let inAppNotificationSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.inAppNotificationSettings)?.get(InAppNotificationSettings.self) { switch inAppNotificationSettings.totalUnreadCountDisplayStyle { case .filtered: filter = true } } return engine.messages.getRelativeUnreadChatListIndex(filtered: filter, position: position, groupId: groupId) } |> switchToLatest } public func selectChat(_ option: ChatListSelectionOption) { guard let interaction = self.interaction else { return } guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else { return } guard let range = self.displayedItemRange.loadedRange else { return } let entryCount = chatListView.filteredEntries.count var current: (EngineChatList.Item.Index, EnginePeer, Int)? = nil var previous: (EngineChatList.Item.Index, EnginePeer)? = nil var next: (EngineChatList.Item.Index, EnginePeer)? = nil outer: for i in range.firstIndex ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue } switch chatListView.filteredEntries[entryCount - i - 1] { case let .PeerEntry(peerEntry): if interaction.highlightedChatLocation?.location == ChatLocation.peer(id: peerEntry.peer.peerId) { current = (peerEntry.index, peerEntry.peer.peer!, entryCount - i - 1) break outer } default: break } } switch option { case .previous(unread: true), .next(unread: true): let position: EngineChatList.RelativePosition if let current = current { if case .previous = option { position = .earlier(than: current.0) } else { position = .later(than: current.0) } } else { position = .later(than: nil) } let engine = self.context.engine let _ = (relativeUnreadChatListIndex(position: position) |> mapToSignal { index -> Signal<(EngineChatList.Item.Index, EnginePeer)?, NoError> in if case let .chatList(index) = index { return engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: index.messageIndex.id.peerId)) |> map { peer -> (EngineChatList.Item.Index, EnginePeer)? in return peer.flatMap { peer -> (EngineChatList.Item.Index, EnginePeer)? in (.chatList(index), peer) } } } else { return .single(nil) } } |> deliverOnMainQueue).start(next: { [weak self] indexAndPeer in guard let strongSelf = self, let (index, peer) = indexAndPeer else { return } let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .chatList(.absoluteLowerBound), scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter) strongSelf.setChatListLocation(location) strongSelf.peerSelected?(peer, nil, false, false, nil) }) case .previous(unread: false), .next(unread: false): var target: (EngineChatList.Item.Index, EnginePeer)? = nil if let current = current, entryCount > 1 { if current.2 > 0, case let .PeerEntry(peerEntry) = chatListView.filteredEntries[current.2 - 1] { next = (peerEntry.index, peerEntry.peer.peer!) } if current.2 <= entryCount - 2, case let .PeerEntry(peerEntry) = chatListView.filteredEntries[current.2 + 1] { previous = (peerEntry.index, peerEntry.peer.peer!) } if case .previous = option { target = previous } else { target = next } } else if entryCount > 0 { if case let .PeerEntry(peerEntry) = chatListView.filteredEntries[entryCount - 1] { target = (peerEntry.index, peerEntry.peer.peer!) } } if let target = target { let location: ChatListNodeLocation = .scroll(index: target.0, sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .center(.top), animated: true, filter: self.chatListFilter) self.setChatListLocation(location) self.peerSelected?(target.1, nil, false, false, nil) } case let .peerId(peerId): let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self, let peer = peer else { return } strongSelf.peerSelected?(peer, nil, false, false, nil) }) case let .index(index): guard index < 10 else { return } let _ = (self.chatListFilterValue.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] filter in guard let self = self else { return } guard case let .chatList(groupId) = self.location else { return } let _ = (chatListViewForLocation(chatListLocation: .chatList(groupId: groupId), location: .initial(count: 10, filter: filter), account: self.context.account) |> take(1) |> deliverOnMainQueue).start(next: { update in let items = update.list.items if items.count > index { let item = items[9 - index - 1] let location: ChatListNodeLocation = .scroll(index: item.index, sourceIndex: .chatList(.absoluteLowerBound), scrollPosition: .center(.top), animated: true, filter: filter) self.setChatListLocation(location) self.peerSelected?(EnginePeer(item.renderedPeer.peer!._asPeer()), nil, false, false, nil) } }) }) } } private func enqueueHistoryPreloadUpdate() { } public func updateSelectedChatLocation(_ chatLocation: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) { guard let interaction = self.interaction else { return } if let chatLocation = chatLocation { interaction.highlightedChatLocation = ChatListHighlightedLocation(location: chatLocation, progress: progress) } else { interaction.highlightedChatLocation = nil } self.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatListItemNode { itemNode.updateIsHighlighted(transition: transition) } } } private func currentlyVisibleLatestChatListIndex() -> EngineChatList.Item.Index? { guard let chatListView = (self.opaqueTransactionState as? ChatListOpaqueTransactionState)?.chatListView else { return nil } if let range = self.displayedItemRange.visibleRange { let entryCount = chatListView.filteredEntries.count for i in range.firstIndex ..< range.lastIndex { if i < 0 || i >= entryCount { assertionFailure() continue } switch chatListView.filteredEntries[entryCount - i - 1] { case let .PeerEntry(peerEntry): return peerEntry.index default: break } } } return nil } private func peerAtPoint(_ point: CGPoint) -> EnginePeer? { var resultPeer: EnginePeer? self.forEachVisibleItemNode { itemNode in if resultPeer == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) { if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { switch item.content { case let .peer(peerData): resultPeer = peerData.peer.peer default: break } } } } return resultPeer } private func threadIdAtPoint(_ point: CGPoint) -> Int64? { var resultThreadId: Int64? self.forEachVisibleItemNode { itemNode in if resultThreadId == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) { if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item { switch item.content { case let .peer(peerData): resultThreadId = peerData.threadInfo?.id default: break } } } } return resultThreadId } private var selectionPanState: (selecting: Bool, initialPeerId: EnginePeer.Id, toggledPeerIds: [[EnginePeer.Id]])? private var threadSelectionPanState: (selecting: Bool, initialThreadId: Int64, toggledThreadIds: [[Int64]])? private var selectionScrollActivationTimer: SwiftSignalKit.Timer? private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator? private var selectionScrollDelta: CGFloat? private var selectionLastLocation: CGPoint? @objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void { let location = recognizer.location(in: self.view) switch recognizer.state { case .began: switch self .location { case .chatList: if let peer = self.peerAtPoint(location) { let selecting = !self.currentState.selectedPeerIds.contains(peer.id) self.selectionPanState = (selecting, peer.id, []) self.interaction?.togglePeersSelection([.peer(peer)], selecting) } case .forum: if let threadId = self.threadIdAtPoint(location) { let selecting = !self.currentState.selectedThreadIds.contains(threadId) self.threadSelectionPanState = (selecting, threadId, []) self.interaction?.toggleThreadsSelection([threadId], selecting) } } case .changed: self.handlePanSelection(location: location) self.selectionLastLocation = location case .ended, .failed, .cancelled: self.threadSelectionPanState = nil self.selectionPanState = nil self.selectionScrollDisplayLink = nil self.selectionScrollActivationTimer?.invalidate() self.selectionScrollActivationTimer = nil self.selectionScrollDelta = nil self.selectionLastLocation = nil self.selectionScrollSkipUpdate = false case .possible: break @unknown default: fatalError() } } private func handlePanSelection(location: CGPoint) { var location = location if location.y < self.insets.top { location.y = self.insets.top + 5.0 } else if location.y > self.frame.height - self.insets.bottom { location.y = self.frame.height - self.insets.bottom - 5.0 } var hasState = false switch self.location { case .chatList: if let state = self.selectionPanState { hasState = true if let peer = self.peerAtPoint(location) { if peer.id == state.initialPeerId { if !state.toggledPeerIds.isEmpty { self.interaction?.togglePeersSelection(state.toggledPeerIds.flatMap { $0.compactMap({ .peerId($0) }) }, !state.selecting) self.selectionPanState = (state.selecting, state.initialPeerId, []) } } else if state.toggledPeerIds.last?.first != peer.id { var updatedToggledPeerIds: [[EnginePeer.Id]] = [] var previouslyToggled = false for i in (0 ..< state.toggledPeerIds.count) { if let peerId = state.toggledPeerIds[i].first { if peerId == peer.id { previouslyToggled = true updatedToggledPeerIds = Array(state.toggledPeerIds.prefix(i + 1)) let peerIdsToToggle = Array(state.toggledPeerIds.suffix(state.toggledPeerIds.count - i - 1)).flatMap { $0 } self.interaction?.togglePeersSelection(peerIdsToToggle.compactMap { .peerId($0) }, !state.selecting) break } } } if !previouslyToggled { updatedToggledPeerIds = state.toggledPeerIds let isSelected = self.currentState.selectedPeerIds.contains(peer.id) if state.selecting != isSelected { updatedToggledPeerIds.append([peer.id]) self.interaction?.togglePeersSelection([.peer(peer)], state.selecting) } } self.selectionPanState = (state.selecting, state.initialPeerId, updatedToggledPeerIds) } } } case .forum: if let state = self.threadSelectionPanState { hasState = true if let threadId = self.threadIdAtPoint(location) { if threadId == state.initialThreadId { if !state.toggledThreadIds.isEmpty { self.interaction?.toggleThreadsSelection(Array(state.toggledThreadIds.joined()), !state.selecting) self.threadSelectionPanState = (state.selecting, state.initialThreadId, []) } } else if state.toggledThreadIds.last?.first != threadId { var updatedToggledThreadIds: [[Int64]] = [] var previouslyToggled = false for i in (0 ..< state.toggledThreadIds.count) { if let toggledThreadId = state.toggledThreadIds[i].first { if toggledThreadId == threadId { previouslyToggled = true updatedToggledThreadIds = Array(state.toggledThreadIds.prefix(i + 1)) let threadIdsToToggle = Array(state.toggledThreadIds.suffix(state.toggledThreadIds.count - i - 1)).flatMap { $0 } self.interaction?.toggleThreadsSelection(threadIdsToToggle.compactMap { $0 }, !state.selecting) break } } } if !previouslyToggled { updatedToggledThreadIds = state.toggledThreadIds let isSelected = self.currentState.selectedThreadIds.contains(threadId) if state.selecting != isSelected { updatedToggledThreadIds.append([threadId]) self.interaction?.toggleThreadsSelection([threadId], state.selecting) } } self.threadSelectionPanState = (state.selecting, state.initialThreadId, updatedToggledThreadIds) } } } } guard hasState else { return } let scrollingAreaHeight: CGFloat = 50.0 if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom { if location.y < self.frame.height / 2.0 { self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight } else { self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight } if let displayLink = self.selectionScrollDisplayLink { displayLink.isPaused = false } else { if let _ = self.selectionScrollActivationTimer { } else { let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in self?.setupSelectionScrolling() }, queue: .mainQueue()) timer.start() self.selectionScrollActivationTimer = timer } } } else { self.selectionScrollDisplayLink?.isPaused = true self.selectionScrollActivationTimer?.invalidate() self.selectionScrollActivationTimer = nil } } private var selectionScrollSkipUpdate = false private func setupSelectionScrolling() { self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in self?.selectionScrollActivationTimer = nil if let strongSelf = self, let delta = strongSelf.selectionScrollDelta { let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta)) let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down let _ = strongSelf.scrollWithDirection(direction, distance: distance) if let location = strongSelf.selectionLastLocation { if !strongSelf.selectionScrollSkipUpdate { strongSelf.handlePanSelection(location: location) } strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate } } }) self.selectionScrollDisplayLink?.isPaused = false } } private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (String, Bool, Bool, ContactsPeerItemStatus.Icon?)? { if accountPeerId == peer.id { return nil } if displayAutoremoveTimeout { if let autoremoveTimeout = autoremoveTimeout { return (strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string, false, true, .autoremove) } else { return (strings.ChatList_LabelAutodeleteDisabled, false, false, .autoremove) } } if let chatListFilters = chatListFilters { var result = "" for case let .filter(_, title, _, data) in chatListFilters { let predicate = chatListFilterPredicate(filter: data) if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { if !result.isEmpty { result.append(", ") } result.append(title) } } if result.isEmpty { return nil } else { return (result, true, false, nil) } } if peer.id.isReplies { return nil } else if case let .user(user) = peer { if user.botInfo != nil || user.flags.contains(.isSupport) { return (strings.ChatList_PeerTypeBot, false, false, nil) } else { if isContact { return (strings.ChatList_PeerTypeContact, false, false, nil) } else { return (strings.ChatList_PeerTypeNonContact, false, false, nil) } } } else if case .secretChat = peer { if isContact { return (strings.ChatList_PeerTypeContact, false, false, nil) } else { return (strings.ChatList_PeerTypeNonContact, false, false, nil) } } else if case .legacyGroup = peer { return (strings.ChatList_PeerTypeGroup, false, false, nil) } else if case let .channel(channel) = peer { if case .group = channel.info { return (strings.ChatList_PeerTypeGroup, false, false, nil) } else { return (strings.ChatList_PeerTypeChannel, false, false, nil) } } return (strings.ChatList_PeerTypeNonContact, false, false, nil) } public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { private let selectionGestureActivationThreshold: CGFloat = 5.0 var recognized: Bool? = nil var initialLocation: CGPoint = CGPoint() public var shouldBegin: (() -> Bool)? public override init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.minimumNumberOfTouches = 2 self.maximumNumberOfTouches = 2 } public override func reset() { super.reset() self.recognized = nil } public override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) if let shouldBegin = self.shouldBegin, !shouldBegin() { self.state = .failed } else { let touch = touches.first! self.initialLocation = touch.location(in: self.view) } } public override func touchesMoved(_ touches: Set, with event: UIEvent) { let location = touches.first!.location(in: self.view) let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y) let touchesArray = Array(touches) if self.recognized == nil, touchesArray.count == 2 { if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last { let firstLocation = firstTouch.location(in: self.view) let secondLocation = secondTouch.location(in: self.view) func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat { let dx = v1.x - v2.x let dy = v1.y - v2.y return sqrt(dx * dx + dy * dy) } if distance(firstLocation, secondLocation) > 200.0 { self.state = .failed } } if self.state != .failed && (abs(translation.y) >= selectionGestureActivationThreshold) { self.recognized = true } } if let recognized = self.recognized, recognized { super.touchesMoved(touches, with: event) } } } func hideChatListContacts(context: AccountContext) { let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).start() }