mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Add empty chat list contacts
This commit is contained in:
parent
85eb44f93e
commit
c206824fee
@ -2000,6 +2000,36 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
let _ = value
|
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
|
self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
|
||||||
|
278
submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift
Normal file
278
submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift
Normal file
@ -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<Void, NoError>?, (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<Void, NoError>?, (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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -616,8 +616,44 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
hiddenOffset: hiddenByDefault && !revealed,
|
hiddenOffset: hiddenByDefault && !revealed,
|
||||||
interaction: nodeInteraction
|
interaction: nodeInteraction
|
||||||
), directionHint: entry.directionHint)
|
), 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):
|
case let .ArchiveIntro(presentationData):
|
||||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
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):
|
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
|
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 {
|
switch action {
|
||||||
@ -881,8 +917,44 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
hiddenOffset: hiddenByDefault && !revealed,
|
hiddenOffset: hiddenByDefault && !revealed,
|
||||||
interaction: nodeInteraction
|
interaction: nodeInteraction
|
||||||
), directionHint: entry.directionHint)
|
), 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):
|
case let .ArchiveIntro(presentationData):
|
||||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
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):
|
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
|
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 {
|
switch action {
|
||||||
@ -1702,6 +1774,65 @@ public final class ChatListNode: ListView {
|
|||||||
let _ = self.enqueueTransition(value).start()
|
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<EngineContactList, NoError> 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(
|
let chatListNodeViewTransition = combineLatest(
|
||||||
queue: viewProcessingQueue,
|
queue: viewProcessingQueue,
|
||||||
hideArchivedFolderByDefault,
|
hideArchivedFolderByDefault,
|
||||||
@ -1711,9 +1842,10 @@ public final class ChatListNode: ListView {
|
|||||||
savedMessagesPeer,
|
savedMessagesPeer,
|
||||||
chatListViewUpdate,
|
chatListViewUpdate,
|
||||||
self.chatFolderUpdates.get() |> distinctUntilChanged,
|
self.chatFolderUpdates.get() |> distinctUntilChanged,
|
||||||
self.statePromise.get()
|
self.statePromise.get(),
|
||||||
|
contacts
|
||||||
)
|
)
|
||||||
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state) -> Signal<ChatListNodeListViewTransition, NoError> in
|
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state, contacts) -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||||
let (update, filter) = updateAndFilter
|
let (update, filter) = updateAndFilter
|
||||||
|
|
||||||
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
|
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
|
||||||
@ -1729,7 +1861,7 @@ public final class ChatListNode: ListView {
|
|||||||
notice = nil
|
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 isEmpty = true
|
||||||
var entries = rawEntries.filter { entry in
|
var entries = rawEntries.filter { entry in
|
||||||
switch entry {
|
switch entry {
|
||||||
@ -1974,6 +2106,9 @@ public final class ChatListNode: ListView {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .ContactEntry:
|
||||||
|
isEmpty = false
|
||||||
|
return true
|
||||||
case .GroupReferenceEntry:
|
case .GroupReferenceEntry:
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
return true
|
return true
|
||||||
@ -2910,7 +3045,7 @@ public final class ChatListNode: ListView {
|
|||||||
var hasArchive = false
|
var hasArchive = false
|
||||||
loop: for entry in transition.chatListView.filteredEntries {
|
loop: for entry in transition.chatListView.filteredEntries {
|
||||||
switch entry {
|
switch entry {
|
||||||
case .GroupReferenceEntry, .HoleEntry, .PeerEntry:
|
case .GroupReferenceEntry, .HoleEntry, .PeerEntry, .ContactEntry:
|
||||||
if case .GroupReferenceEntry = entry {
|
if case .GroupReferenceEntry = entry {
|
||||||
hasArchive = true
|
hasArchive = true
|
||||||
} else {
|
} else {
|
||||||
@ -2929,7 +3064,7 @@ public final class ChatListNode: ListView {
|
|||||||
} else {
|
} else {
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
case .ArchiveIntro, .Notice, .HeaderEntry, .AdditionalCategory:
|
case .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .HeaderEntry, .AdditionalCategory:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3660,3 +3795,7 @@ public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hideChatListContacts(context: AccountContext) {
|
||||||
|
let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).start()
|
||||||
|
}
|
||||||
|
@ -12,7 +12,10 @@ enum ChatListNodeEntryId: Hashable {
|
|||||||
case PeerId(Int64)
|
case PeerId(Int64)
|
||||||
case ThreadId(Int64)
|
case ThreadId(Int64)
|
||||||
case GroupId(EngineChatList.Group)
|
case GroupId(EngineChatList.Group)
|
||||||
|
case ContactId(EnginePeer.Id)
|
||||||
case ArchiveIntro
|
case ArchiveIntro
|
||||||
|
case EmptyIntro
|
||||||
|
case SectionHeader
|
||||||
case Notice
|
case Notice
|
||||||
case additionalCategory(Int)
|
case additionalCategory(Int)
|
||||||
}
|
}
|
||||||
@ -20,6 +23,8 @@ enum ChatListNodeEntryId: Hashable {
|
|||||||
enum ChatListNodeEntrySortIndex: Comparable {
|
enum ChatListNodeEntrySortIndex: Comparable {
|
||||||
case index(EngineChatList.Item.Index)
|
case index(EngineChatList.Item.Index)
|
||||||
case additionalCategory(Int)
|
case additionalCategory(Int)
|
||||||
|
case sectionHeader
|
||||||
|
case contact(id: EnginePeer.Id, presence: EnginePeer.Presence)
|
||||||
|
|
||||||
static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool {
|
static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool {
|
||||||
switch lhs {
|
switch lhs {
|
||||||
@ -29,6 +34,10 @@ enum ChatListNodeEntrySortIndex: Comparable {
|
|||||||
return lhsIndex < rhsIndex
|
return lhsIndex < rhsIndex
|
||||||
case .additionalCategory:
|
case .additionalCategory:
|
||||||
return false
|
return false
|
||||||
|
case .sectionHeader:
|
||||||
|
return true
|
||||||
|
case .contact:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
case let .additionalCategory(lhsIndex):
|
case let .additionalCategory(lhsIndex):
|
||||||
switch rhs {
|
switch rhs {
|
||||||
@ -36,6 +45,30 @@ enum ChatListNodeEntrySortIndex: Comparable {
|
|||||||
return lhsIndex < rhsIndex
|
return lhsIndex < rhsIndex
|
||||||
case .index:
|
case .index:
|
||||||
return true
|
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 HeaderEntry
|
||||||
case PeerEntry(PeerEntryData)
|
case PeerEntry(PeerEntryData)
|
||||||
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
|
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 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 ArchiveIntro(presentationData: ChatListPresentationData)
|
||||||
|
case EmptyIntro(presentationData: ChatListPresentationData)
|
||||||
|
case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool)
|
||||||
case Notice(presentationData: ChatListPresentationData, notice: ChatListNotice)
|
case Notice(presentationData: ChatListPresentationData, notice: ChatListNotice)
|
||||||
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
|
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)))
|
return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex)))
|
||||||
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
|
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
|
||||||
return .index(index)
|
return .index(index)
|
||||||
|
case let .ContactEntry(contactEntry):
|
||||||
|
return .contact(id: contactEntry.peer.id, presence: contactEntry.presence)
|
||||||
case .ArchiveIntro:
|
case .ArchiveIntro:
|
||||||
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor))
|
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:
|
case .Notice:
|
||||||
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor.successor))
|
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor.successor))
|
||||||
case let .AdditionalCategory(index, _, _, _, _, _, _):
|
case let .AdditionalCategory(index, _, _, _, _, _, _):
|
||||||
@ -280,8 +347,14 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
return .Hole(Int64(holeIndex.id.id))
|
return .Hole(Int64(holeIndex.id.id))
|
||||||
case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _):
|
case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _):
|
||||||
return .GroupId(groupId)
|
return .GroupId(groupId)
|
||||||
|
case let .ContactEntry(contactEntry):
|
||||||
|
return .ContactId(contactEntry.peer.id)
|
||||||
case .ArchiveIntro:
|
case .ArchiveIntro:
|
||||||
return .ArchiveIntro
|
return .ArchiveIntro
|
||||||
|
case .EmptyIntro:
|
||||||
|
return .EmptyIntro
|
||||||
|
case .SectionHeader:
|
||||||
|
return .SectionHeader
|
||||||
case .Notice:
|
case .Notice:
|
||||||
return .Notice
|
return .Notice
|
||||||
case let .AdditionalCategory(_, id, _, _, _, _, _):
|
case let .AdditionalCategory(_, id, _, _, _, _, _):
|
||||||
@ -347,6 +420,12 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case let .ContactEntry(contactEntry):
|
||||||
|
if case .ContactEntry(contactEntry) = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
case let .ArchiveIntro(lhsPresentationData):
|
case let .ArchiveIntro(lhsPresentationData):
|
||||||
if case let .ArchiveIntro(rhsPresentationData) = rhs {
|
if case let .ArchiveIntro(rhsPresentationData) = rhs {
|
||||||
if lhsPresentationData !== rhsPresentationData {
|
if lhsPresentationData !== rhsPresentationData {
|
||||||
@ -356,6 +435,27 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
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):
|
case let .Notice(lhsPresentationData, lhsInfo):
|
||||||
if case let .Notice(rhsPresentationData, rhsInfo) = rhs {
|
if case let .Notice(rhsPresentationData, rhsInfo) = rhs {
|
||||||
if lhsPresentationData !== rhsPresentationData {
|
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] = []
|
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
|
var pinnedIndexOffset: UInt16 = 0
|
||||||
|
|
||||||
if !view.hasLater, case .chatList = mode {
|
if !view.hasLater, case .chatList = mode {
|
||||||
@ -668,6 +791,14 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
|||||||
|
|
||||||
if displayArchiveIntro {
|
if displayArchiveIntro {
|
||||||
result.append(.ArchiveIntro(presentationData: state.presentationData))
|
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 {
|
if let notice {
|
||||||
|
@ -123,13 +123,14 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
|||||||
private var timeoutTimer: SwiftSignalKit.Timer?
|
private var timeoutTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
private var padding: CGFloat
|
private var padding: CGFloat
|
||||||
|
private var innerPadding: UIEdgeInsets
|
||||||
|
|
||||||
private var layout: ContainerViewLayout?
|
private var layout: ContainerViewLayout?
|
||||||
private var initialArrowOnBottom: Bool
|
private var initialArrowOnBottom: Bool
|
||||||
|
|
||||||
public var dismissed: ((Bool) -> Void)?
|
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.content = content
|
||||||
self.baseFontSize = baseFontSize
|
self.baseFontSize = baseFontSize
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
@ -138,6 +139,7 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
|||||||
self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate
|
self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate
|
||||||
self.initialArrowOnBottom = arrowOnBottom
|
self.initialArrowOnBottom = arrowOnBottom
|
||||||
self.padding = padding
|
self.padding = padding
|
||||||
|
self.innerPadding = innerPadding
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: nil)
|
super.init(navigationBarPresentationData: nil)
|
||||||
|
|
||||||
@ -157,6 +159,7 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
|||||||
self?.dismiss(tappedInside: tappedInside)
|
self?.dismiss(tappedInside: tappedInside)
|
||||||
}, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource)
|
}, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource)
|
||||||
self.controllerNode.padding = self.padding
|
self.controllerNode.padding = self.padding
|
||||||
|
self.controllerNode.innerPadding = self.innerPadding
|
||||||
self.controllerNode.arrowOnBottom = self.initialArrowOnBottom
|
self.controllerNode.arrowOnBottom = self.initialArrowOnBottom
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ final class TooltipControllerNode: ASDisplayNode {
|
|||||||
var arrowOnBottom: Bool = true
|
var arrowOnBottom: Bool = true
|
||||||
|
|
||||||
var padding: CGFloat = 8.0
|
var padding: CGFloat = 8.0
|
||||||
|
var innerPadding: UIEdgeInsets = UIEdgeInsets()
|
||||||
|
|
||||||
private var dismissedByTouchOutside = false
|
private var dismissedByTouchOutside = false
|
||||||
private var dismissByTapOutsideSource = false
|
private var dismissByTapOutsideSource = false
|
||||||
@ -98,14 +99,14 @@ final class TooltipControllerNode: ASDisplayNode {
|
|||||||
textSize.width = ceil(textSize.width / 2.0) * 2.0
|
textSize.width = ceil(textSize.width / 2.0) * 2.0
|
||||||
textSize.height = ceil(textSize.height / 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 {
|
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))
|
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.imageNode.frame = imageFrame
|
||||||
self.textNode.frame = textFrame
|
self.textNode.frame = textFrame
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import Display
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
private let titleFont = Font.bold(13.0)
|
private let titleFont = Font.bold(13.0)
|
||||||
private let actionFont = Font.medium(13.0)
|
private let actionFont = Font.regular(13.0)
|
||||||
|
|
||||||
public enum ListSectionHeaderActionType {
|
public enum ListSectionHeaderActionType {
|
||||||
case generic
|
case generic
|
||||||
@ -13,6 +13,7 @@ public enum ListSectionHeaderActionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class ListSectionHeaderNode: ASDisplayNode {
|
public final class ListSectionHeaderNode: ASDisplayNode {
|
||||||
|
private let backgroundLayer: SimpleLayer
|
||||||
private let label: ImmediateTextNode
|
private let label: ImmediateTextNode
|
||||||
private var actionButtonLabel: ImmediateTextNode?
|
private var actionButtonLabel: ImmediateTextNode?
|
||||||
private var actionButton: HighlightableButtonNode?
|
private var actionButton: HighlightableButtonNode?
|
||||||
@ -87,16 +88,29 @@ public final class ListSectionHeaderNode: ASDisplayNode {
|
|||||||
public init(theme: PresentationTheme) {
|
public init(theme: PresentationTheme) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
|
self.backgroundLayer = SimpleLayer()
|
||||||
|
|
||||||
self.label = ImmediateTextNode()
|
self.label = ImmediateTextNode()
|
||||||
self.label.isUserInteractionEnabled = false
|
self.label.isUserInteractionEnabled = false
|
||||||
self.label.isAccessibilityElement = true
|
self.label.isAccessibilityElement = true
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.backgroundLayer)
|
||||||
|
|
||||||
self.addSubnode(self.label)
|
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) {
|
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.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 {
|
if let action = self.action {
|
||||||
self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor)
|
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)
|
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)
|
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() {
|
@objc private func actionButtonPressed() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user