From c206824feece2eba80526981958833c4aad9a010 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 11 Apr 2023 23:10:30 +0400 Subject: [PATCH] Add empty chat list contacts --- .../Sources/ChatListController.swift | 30 ++ .../Sources/Node/ChatListEmptyInfoItem.swift | 278 ++++++++++++++++++ .../Sources/Node/ChatListNode.swift | 149 +++++++++- .../Sources/Node/ChatListNodeEntries.swift | 133 ++++++++- .../Display/Source/TooltipController.swift | 5 +- .../Source/TooltipControllerNode.swift | 7 +- .../Sources/ListSectionHeaderNode.swift | 24 +- 7 files changed, 612 insertions(+), 14 deletions(-) create mode 100644 submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 0f02abfe96..a571f13603 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2000,6 +2000,36 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let _ = value }) }) + + Queue.mainQueue().after(2.0, { [weak self] in + guard let self else { + return + } + //TODO:generalize + var hasEmptyMark = false + self.chatListDisplayNode.mainContainerNode.currentItemNode.forEachItemNode { itemNode in + if itemNode is ChatListSectionHeaderNode { + hasEmptyMark = true + } + } + if hasEmptyMark { + if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if let rightButtonView = componentView.rightButtonView { + let absoluteFrame = rightButtonView.convert(rightButtonView.bounds, to: self.view) + //TODO:localize + let text: String = "Send a message or\nstart a group here." + + let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 30.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 6.0, innerPadding: UIEdgeInsets(top: 2.0, left: 3.0, bottom: 2.0, right: 3.0)) + self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + guard let self else { + return nil + } + return (self.displayNode, absoluteFrame.insetBy(dx: 4.0, dy: 8.0).offsetBy(dx: 4.0, dy: -1.0)) + })) + } + } + } + }) } self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in diff --git a/submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift new file mode 100644 index 0000000000..78f28d1400 --- /dev/null +++ b/submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift @@ -0,0 +1,278 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit +import TelegramPresentationData +import ListSectionHeaderNode +import AppBundle +import AnimatedStickerNode +import TelegramAnimatedStickerNode + +class ChatListEmptyInfoItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings + + let selectable: Bool = false + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ChatListEmptyInfoItemNode() + + let (nodeLayout, apply) = node.asyncLayout()(self, params, false) + + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply() + }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ChatListEmptyInfoItemNode) + if let nodeValue = node() as? ChatListEmptyInfoItemNode { + + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params, nextItem == nil) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply() + }) + } + } + } + } + } +} + +class ChatListEmptyInfoItemNode: ListViewItemNode { + private var item: ChatListEmptyInfoItem? + + private let animationNode: AnimatedStickerNode + private let textNode: TextNode + + override var visibility: ListViewItemNodeVisibility { + didSet { + let wasVisible = self.visibilityStatus + let isVisible: Bool + switch self.visibility { + case let .visible(fraction, _): + isVisible = fraction > 0.2 + case .none: + isVisible = false + } + if wasVisible != isVisible { + self.visibilityStatus = isVisible + } + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + self.animationNode.visibility = self.visibilityStatus + } + } + } + + required init() { + self.animationNode = DefaultAnimatedStickerNodeImpl() + self.textNode = TextNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.animationNode) + self.addSubnode(self.textNode) + } + + override func didLoad() { + super.didLoad() + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + let layout = self.asyncLayout() + let (_, apply) = layout(item as! ChatListEmptyInfoItem, params, nextItem == nil) + apply() + } + + func asyncLayout() -> (_ item: ChatListEmptyInfoItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + return { item, params, last in + let baseWidth = params.width - params.leftInset - params.rightInset + + let topInset: CGFloat = 8.0 + let textSpacing: CGFloat = 27.0 + let bottomInset: CGFloat = 24.0 + let animationHeight: CGFloat = 140.0 + + let string = NSMutableAttributedString(string: item.strings.ChatList_EmptyChatList, font: Font.semibold(17.0), textColor: item.theme.list.itemPrimaryTextColor) + + let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: string, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: .greatestFiniteMagnitude), alignment: .center)) + + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: topInset + animationHeight + textSpacing + textLayout.0.size.height + bottomInset), insets: UIEdgeInsets()) + + return (layout, { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.item = item + + var topOffset: CGFloat = topInset + + let animationFrame = CGRect(origin: CGPoint(x: floor((params.width - animationHeight) * 0.5), y: topOffset), size: CGSize(width: animationHeight, height: animationHeight)) + if strongSelf.animationNode.bounds.isEmpty { + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListEmpty"), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + } + strongSelf.animationNode.frame = animationFrame + topOffset += animationHeight + textSpacing + + let _ = textLayout.1() + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: topOffset), size: textLayout.0.size) + + strongSelf.contentSize = layout.contentSize + strongSelf.insets = layout.insets + }) + } + } +} + +class ChatListSectionHeaderItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings + let hide: (() -> Void)? + + let selectable: Bool = false + + init(theme: PresentationTheme, strings: PresentationStrings, hide: (() -> Void)?) { + self.theme = theme + self.strings = strings + self.hide = hide + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ChatListSectionHeaderNode() + + let (nodeLayout, apply) = node.asyncLayout()(self, params, false) + + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply() + }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ChatListSectionHeaderNode) + if let nodeValue = node() as? ChatListSectionHeaderNode { + + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params, nextItem == nil) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply() + }) + } + } + } + } + } +} + +class ChatListSectionHeaderNode: ListViewItemNode { + private var item: ChatListSectionHeaderItem? + + private var headerNode: ListSectionHeaderNode? + + required init() { + super.init(layerBacked: false, dynamicBounce: false) + + self.zPosition = 1.0 + } + + override func didLoad() { + super.didLoad() + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + let layout = self.asyncLayout() + let (_, apply) = layout(item as! ChatListSectionHeaderItem, params, nextItem == nil) + apply() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let headerNode = self.headerNode { + if let result = headerNode.view.hitTest(self.view.convert(point, to: headerNode.view), with: event) { + return result + } + } + return nil + } + + func asyncLayout() -> (_ item: ChatListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) { + return { item, params, last in + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 28.0), insets: UIEdgeInsets()) + + return (layout, { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.item = item + + let headerNode: ListSectionHeaderNode + if let current = strongSelf.headerNode { + headerNode = current + } else { + headerNode = ListSectionHeaderNode(theme: item.theme) + strongSelf.headerNode = headerNode + strongSelf.addSubnode(headerNode) + } + + //TODO:localize + headerNode.title = "YOUR CONTACTS ON TELEGRAM" + if item.hide != nil { + headerNode.action = "hide" + headerNode.actionType = .generic + headerNode.activateAction = { + guard let self else { + return + } + self.item?.hide?() + } + } else { + headerNode.action = nil + } + + headerNode.updateTheme(theme: item.theme) + headerNode.updateLayout(size: CGSize(width: params.width, height: layout.contentSize.height), leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.contentSize = layout.contentSize + strongSelf.insets = layout.insets + }) + } + } +} + diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 24506af387..6cdfc5f62c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -616,8 +616,44 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL hiddenOffset: hiddenByDefault && !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 { @@ -881,8 +917,44 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL hiddenOffset: hiddenByDefault && !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 { @@ -1702,6 +1774,65 @@ public final class ChatListNode: ListView { let _ = self.enqueueTransition(value).start() })*/ + let contacts: Signal<[ChatListContactPeer], NoError> + if case .chatList(groupId: .root) = location, chatListFilter == nil { + 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 chatListNodeViewTransition = combineLatest( queue: viewProcessingQueue, hideArchivedFolderByDefault, @@ -1711,9 +1842,10 @@ public final class ChatListNode: ListView { savedMessagesPeer, chatListViewUpdate, self.chatFolderUpdates.get() |> distinctUntilChanged, - self.statePromise.get() + self.statePromise.get(), + contacts ) - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state) -> Signal in + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state, contacts) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) @@ -1729,7 +1861,7 @@ public final class ChatListNode: ListView { notice = nil } - let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location) + let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts) var isEmpty = true var entries = rawEntries.filter { entry in switch entry { @@ -1974,6 +2106,9 @@ public final class ChatListNode: ListView { return false } } + case .ContactEntry: + isEmpty = false + return true case .GroupReferenceEntry: isEmpty = false return true @@ -2910,7 +3045,7 @@ public final class ChatListNode: ListView { var hasArchive = false loop: for entry in transition.chatListView.filteredEntries { switch entry { - case .GroupReferenceEntry, .HoleEntry, .PeerEntry: + case .GroupReferenceEntry, .HoleEntry, .PeerEntry, .ContactEntry: if case .GroupReferenceEntry = entry { hasArchive = true } else { @@ -2929,7 +3064,7 @@ public final class ChatListNode: ListView { } else { break loop } - case .ArchiveIntro, .Notice, .HeaderEntry, .AdditionalCategory: + case .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .HeaderEntry, .AdditionalCategory: break } } @@ -3660,3 +3795,7 @@ public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { } } } + +func hideChatListContacts(context: AccountContext) { + let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).start() +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 8d9b8d8300..43d34c20ad 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -12,7 +12,10 @@ enum ChatListNodeEntryId: Hashable { case PeerId(Int64) case ThreadId(Int64) case GroupId(EngineChatList.Group) + case ContactId(EnginePeer.Id) case ArchiveIntro + case EmptyIntro + case SectionHeader case Notice case additionalCategory(Int) } @@ -20,6 +23,8 @@ enum ChatListNodeEntryId: Hashable { enum ChatListNodeEntrySortIndex: Comparable { case index(EngineChatList.Item.Index) case additionalCategory(Int) + case sectionHeader + case contact(id: EnginePeer.Id, presence: EnginePeer.Presence) static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool { switch lhs { @@ -29,6 +34,10 @@ enum ChatListNodeEntrySortIndex: Comparable { return lhsIndex < rhsIndex case .additionalCategory: return false + case .sectionHeader: + return true + case .contact: + return true } case let .additionalCategory(lhsIndex): switch rhs { @@ -36,6 +45,30 @@ enum ChatListNodeEntrySortIndex: Comparable { return lhsIndex < rhsIndex case .index: return true + case .sectionHeader: + return true + case .contact: + return true + } + case .sectionHeader: + switch rhs { + case .additionalCategory, .index, .sectionHeader: + return false + case .contact: + return true + } + case let .contact(lhsId, lhsPresense): + switch rhs { + case .sectionHeader: + return false + case let .contact(rhsId, rhsPresense): + if lhsPresense != rhsPresense { + return rhsPresense.status > rhsPresense.status + } else { + return lhsId < rhsId + } + default: + return false } } } @@ -238,11 +271,39 @@ enum ChatListNodeEntry: Comparable, Identifiable { } } + struct ContactEntryData: Equatable { + var presentationData: ChatListPresentationData + var peer: EnginePeer + var presence: EnginePeer.Presence + + init(presentationData: ChatListPresentationData, peer: EnginePeer, presence: EnginePeer.Presence) { + self.presentationData = presentationData + self.peer = peer + self.presence = presence + } + + static func ==(lhs: ContactEntryData, rhs: ContactEntryData) -> Bool { + if lhs.presentationData !== rhs.presentationData { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.presence != rhs.presence { + return false + } + return true + } + } + case HeaderEntry case PeerEntry(PeerEntryData) case HoleEntry(EngineMessage.Index, theme: PresentationTheme) case GroupReferenceEntry(index: EngineChatList.Item.Index, presentationData: ChatListPresentationData, groupId: EngineChatList.Group, peers: [EngineChatList.GroupItem.Item], message: EngineMessage?, editing: Bool, unreadCount: Int, revealed: Bool, hiddenByDefault: Bool) + case ContactEntry(ContactEntryData) case ArchiveIntro(presentationData: ChatListPresentationData) + case EmptyIntro(presentationData: ChatListPresentationData) + case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool) case Notice(presentationData: ChatListPresentationData, notice: ChatListNotice) case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData) @@ -256,8 +317,14 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex))) case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _): return .index(index) + case let .ContactEntry(contactEntry): + return .contact(id: contactEntry.peer.id, presence: contactEntry.presence) case .ArchiveIntro: return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor)) + case .EmptyIntro: + return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor)) + case .SectionHeader: + return .sectionHeader case .Notice: return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor.successor)) case let .AdditionalCategory(index, _, _, _, _, _, _): @@ -280,8 +347,14 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .Hole(Int64(holeIndex.id.id)) case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _): return .GroupId(groupId) + case let .ContactEntry(contactEntry): + return .ContactId(contactEntry.peer.id) case .ArchiveIntro: return .ArchiveIntro + case .EmptyIntro: + return .EmptyIntro + case .SectionHeader: + return .SectionHeader case .Notice: return .Notice case let .AdditionalCategory(_, id, _, _, _, _, _): @@ -347,6 +420,12 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } + case let .ContactEntry(contactEntry): + if case .ContactEntry(contactEntry) = rhs { + return true + } else { + return false + } case let .ArchiveIntro(lhsPresentationData): if case let .ArchiveIntro(rhsPresentationData) = rhs { if lhsPresentationData !== rhsPresentationData { @@ -356,6 +435,27 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } + case let .EmptyIntro(lhsPresentationData): + if case let .EmptyIntro(rhsPresentationData) = rhs { + if lhsPresentationData !== rhsPresentationData { + return false + } + return true + } else { + return false + } + case let .SectionHeader(lhsPresentationData, lhsDisplayHide): + if case let .SectionHeader(rhsPresentationData, rhsDisplayHide) = rhs { + if lhsPresentationData !== rhsPresentationData { + return false + } + if lhsDisplayHide != rhsDisplayHide { + return false + } + return true + } else { + return false + } case let .Notice(lhsPresentationData, lhsInfo): if case let .Notice(rhsPresentationData, rhsInfo) = rhs { if lhsPresentationData !== rhsPresentationData { @@ -407,9 +507,32 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1 } } -func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) { +struct ChatListContactPeer { + var peer: EnginePeer + var presence: EnginePeer.Presence + + init(peer: EnginePeer, presence: EnginePeer.Presence) { + self.peer = peer + self.presence = presence + } +} + +func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer]) -> (entries: [ChatListNodeEntry], loading: Bool) { var result: [ChatListNodeEntry] = [] + if !view.hasEarlier { + for contact in contacts { + result.append(.ContactEntry(ChatListNodeEntry.ContactEntryData( + presentationData: state.presentationData, + peer: contact.peer, + presence: contact.presence + ))) + } + if !contacts.isEmpty { + result.append(.SectionHeader(presentationData: state.presentationData, displayHide: !view.items.isEmpty)) + } + } + var pinnedIndexOffset: UInt16 = 0 if !view.hasLater, case .chatList = mode { @@ -668,6 +791,14 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState if displayArchiveIntro { result.append(.ArchiveIntro(presentationData: state.presentationData)) + } else if !contacts.isEmpty && !result.contains(where: { entry in + if case .PeerEntry = entry { + return true + } else { + return false + } + }) { + result.append(.EmptyIntro(presentationData: state.presentationData)) } if let notice { diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index 1ab86d59ef..be5d52bff7 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -123,13 +123,14 @@ open class TooltipController: ViewController, StandalonePresentableController { private var timeoutTimer: SwiftSignalKit.Timer? private var padding: CGFloat + private var innerPadding: UIEdgeInsets private var layout: ContainerViewLayout? private var initialArrowOnBottom: Bool public var dismissed: ((Bool) -> Void)? - public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0) { + public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0, innerPadding: UIEdgeInsets = UIEdgeInsets()) { self.content = content self.baseFontSize = baseFontSize self.timeout = timeout @@ -138,6 +139,7 @@ open class TooltipController: ViewController, StandalonePresentableController { self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate self.initialArrowOnBottom = arrowOnBottom self.padding = padding + self.innerPadding = innerPadding super.init(navigationBarPresentationData: nil) @@ -157,6 +159,7 @@ open class TooltipController: ViewController, StandalonePresentableController { self?.dismiss(tappedInside: tappedInside) }, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource) self.controllerNode.padding = self.padding + self.controllerNode.innerPadding = self.innerPadding self.controllerNode.arrowOnBottom = self.initialArrowOnBottom self.displayNodeDidLoad() } diff --git a/submodules/Display/Source/TooltipControllerNode.swift b/submodules/Display/Source/TooltipControllerNode.swift index d044cbc5f6..7122720407 100644 --- a/submodules/Display/Source/TooltipControllerNode.swift +++ b/submodules/Display/Source/TooltipControllerNode.swift @@ -20,6 +20,7 @@ final class TooltipControllerNode: ASDisplayNode { var arrowOnBottom: Bool = true var padding: CGFloat = 8.0 + var innerPadding: UIEdgeInsets = UIEdgeInsets() private var dismissedByTouchOutside = false private var dismissByTapOutsideSource = false @@ -98,14 +99,14 @@ final class TooltipControllerNode: ASDisplayNode { textSize.width = ceil(textSize.width / 2.0) * 2.0 textSize.height = ceil(textSize.height / 2.0) * 2.0 - contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0) + contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0 + self.innerPadding.left + self.innerPadding.right, height: textSize.height + 34.0 + self.innerPadding.top + self.innerPadding.bottom) - let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize) + let textFrame = CGRect(origin: CGPoint(x: 6.0 + self.innerPadding.left + imageSizeWithInset.width, y: 17.0 + self.innerPadding.top), size: textSize) if transition.isAnimated, textFrame.size != self.textNode.frame.size { transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0)) } - let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: self.innerPadding.left + 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize) self.imageNode.frame = imageFrame self.textNode.frame = textFrame } diff --git a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift index d737a6a77f..bbd1942090 100644 --- a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift +++ b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift @@ -5,7 +5,7 @@ import Display import TelegramPresentationData private let titleFont = Font.bold(13.0) -private let actionFont = Font.medium(13.0) +private let actionFont = Font.regular(13.0) public enum ListSectionHeaderActionType { case generic @@ -13,6 +13,7 @@ public enum ListSectionHeaderActionType { } public final class ListSectionHeaderNode: ASDisplayNode { + private let backgroundLayer: SimpleLayer private let label: ImmediateTextNode private var actionButtonLabel: ImmediateTextNode? private var actionButton: HighlightableButtonNode? @@ -87,16 +88,29 @@ public final class ListSectionHeaderNode: ASDisplayNode { public init(theme: PresentationTheme) { self.theme = theme + self.backgroundLayer = SimpleLayer() + self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false self.label.isAccessibilityElement = true super.init() - + self.layer.addSublayer(self.backgroundLayer) + self.addSubnode(self.label) - self.backgroundColor = theme.chatList.sectionHeaderFillColor + self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let actionButton = self.actionButton { + if actionButton.frame.contains(point) { + return actionButton.view + } + } + + return super.hitTest(point, with: event) } public func updateTheme(theme: PresentationTheme) { @@ -105,7 +119,7 @@ public final class ListSectionHeaderNode: ASDisplayNode { self.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor) - self.backgroundColor = theme.chatList.sectionHeaderFillColor + self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor if let action = self.action { self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor) } @@ -126,6 +140,8 @@ public final class ListSectionHeaderNode: ASDisplayNode { actionButtonLabel.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 16.0 - buttonSize.width, y: 6.0 + UIScreenPixel), size: buttonSize) actionButton.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 16.0 - buttonSize.width, y: 6.0 + UIScreenPixel), size: buttonSize) } + + self.backgroundLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel)) } @objc private func actionButtonPressed() {