Various improvements

This commit is contained in:
Isaac 2026-03-31 22:42:02 +08:00
parent 3e0c38c3ce
commit 796826b0ad
12 changed files with 766 additions and 804 deletions

View file

@ -67,6 +67,7 @@ public final class PeerSelectionControllerParams {
public let hasCreation: Bool
public let immediatelySwitchToContacts: Bool
public let immediatelyActivateMultipleSelection: Bool
public let suggestedPeers: [EnginePeer]
public init(
context: AccountContext,
@ -89,7 +90,8 @@ public final class PeerSelectionControllerParams {
selectForumThreads: Bool = false,
hasCreation: Bool = false,
immediatelySwitchToContacts: Bool = false,
immediatelyActivateMultipleSelection: Bool = false
immediatelyActivateMultipleSelection: Bool = false,
suggestedPeers: [EnginePeer] = []
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
@ -112,6 +114,7 @@ public final class PeerSelectionControllerParams {
self.hasCreation = hasCreation
self.immediatelySwitchToContacts = immediatelySwitchToContacts
self.immediatelyActivateMultipleSelection = immediatelyActivateMultipleSelection
self.suggestedPeers = suggestedPeers
}
}

View file

@ -27,7 +27,7 @@ import GlobalControlPanelsContext
public enum ChatListNodeMode {
case chatList(appendContacts: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], topPeers: [EnginePeer], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool)
case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool)
}
@ -372,286 +372,268 @@ public struct ChatListNodeState: Equatable {
}
}
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry], presentationData: ChatListPresentationData) -> [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)
}
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, dateTimeFormat: presentationData.dateTimeFormat),
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 hasUnseenPollVotes = peerEntry.hasUnseenPollVotes
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,
hasUnseenPollVotes: hasUnseenPollVotes,
draftState: draftState,
mediaDraftContentType: peerEntry.mediaDraftContentType,
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
)
},
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
displayAsTopicList: peerEntry.displayAsTopicList,
tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
)),
editing: editing,
hasActiveRevealControls: hasActiveRevealControls,
selected: selected,
header: nil,
enabledContextActions: .auto,
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatOrMonoforumMainPeer
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
}
if peerEntry.requiresPremiumForMessaging {
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
}
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
canManage = true
} else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) {
canManage = true
}
if canManage {
} 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.isForumOrMonoForum {
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, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
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, false)
}
}
}, disabledAction: (isForum && editing) && !peerEntry.requiresPremiumForMessaging ? nil : { _ in
if let chatPeer = chatPeer {
nodeInteraction.disabledPeerSelected(chatPeer, threadId, peerEntry.requiresPremiumForMessaging ? .premiumRequired : .generic)
}
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
context: context,
title: title,
image: image,
appearance: appearance,
isSelected: selected,
header: header,
action: {
nodeInteraction.additionalCategorySelected(id)
}
), directionHint: entry.directionHint)
case let .TopPeer(_, 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, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch(isSavedMessages: false),
peer: .peer(peer: peer, chatPeer: peer),
status: .none,
requiresPremiumForMessaging: false,
enabled: true,
selection: .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
index: nil,
header: nil,
action: { _ in
nodeInteraction.peerSelected(peer, nil, nil, nil, false)
},
disabledAction: nil,
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), 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 hasUnseenPollVotes = peerEntry.hasUnseenPollVotes
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,
hasUnseenPollVotes: hasUnseenPollVotes,
draftState: draftState,
mediaDraftContentType: peerEntry.mediaDraftContentType,
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
)
},
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case .peerType:
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
displayAsTopicList: peerEntry.displayAsTopicList,
tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters)
)),
editing: editing,
hasActiveRevealControls: hasActiveRevealControls,
selected: selected,
header: nil,
enabledContextActions: .auto,
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatOrMonoforumMainPeer
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
}
if peerEntry.requiresPremiumForMessaging {
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
}
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
canManage = true
} else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) {
canManage = true
}
if canManage {
} 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
}
}
}
}
let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer)
let status: ContactsPeerItemStatus = .none
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.isForumOrMonoForum {
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, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
@ -661,69 +643,37 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
peer: peerContent,
status: status,
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
enabled: true,
selection: editing ? .selectable(selected: selected) : .none,
enabled: enabled,
selection: selectable ? .selectable(selected: selected) : .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
index: nil,
header: nil,
header: header,
action: { _ in
if let chatPeer = chatPeer {
if editing {
nodeInteraction.togglePeerSelected(chatPeer, nil)
nodeInteraction.togglePeerSelected(chatPeer, threadId)
} else {
nodeInteraction.peerSelected(chatPeer, nil, nil, nil, false)
nodeInteraction.peerSelected(chatPeer, nil, threadId, nil, false)
}
}
}, disabledAction: peerEntry.requiresPremiumForMessaging ? { _ in
if let chatPeer {
nodeInteraction.disabledPeerSelected(chatPeer, nil, .premiumRequired)
}, disabledAction: (isForum && editing) && !peerEntry.requiresPremiumForMessaging ? nil : { _ in
if let chatPeer = chatPeer {
nodeInteraction.disabledPeerSelected(chatPeer, threadId, peerEntry.requiresPremiumForMessaging ? .premiumRequired : .generic)
}
} : nil,
},
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case .peerType:
let itemPeer = peer.chatOrMonoforumMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
}
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,
appearsPinned: groupReferenceEntry.appearsPinned,
storyState: groupReferenceEntry.storyState.flatMap { storyState in
return ChatListItemContent.StoryState(
stats: storyState.stats,
hasUnseenCloseFriends: storyState.hasUnseenCloseFriends
)
}
)),
editing: groupReferenceEntry.editing,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: .auto,
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)
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, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
@ -732,31 +682,103 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
peerMode: .generalSearch(isSavedMessages: false),
peer: peerContent,
status: status,
requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging,
enabled: true,
selection: .none,
selection: editing ? .selectable(selected: selected) : .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
index: nil,
header: header,
header: nil,
action: { _ in
nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil, false)
},
disabledAction: nil,
if let chatPeer = chatPeer {
if editing {
nodeInteraction.togglePeerSelected(chatPeer, nil)
} else {
nodeInteraction.peerSelected(chatPeer, nil, nil, nil, false)
}
}
}, disabledAction: peerEntry.requiresPremiumForMessaging ? { _ in
if let chatPeer {
nodeInteraction.disabledPeerSelected(chatPeer, nil, .premiumRequired)
}
} : 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 .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,
appearsPinned: groupReferenceEntry.appearsPinned,
storyState: groupReferenceEntry.storyState.flatMap { storyState in
return ChatListItemContent.StoryState(
stats: storyState.stats,
hasUnseenCloseFriends: storyState.hasUnseenCloseFriends
)
}
)),
editing: groupReferenceEntry.editing,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: .auto,
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, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch(isSavedMessages: false),
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, false)
},
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)
}
}
}
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry], presentationData: ChatListPresentationData) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .PeerEntry(peerEntry):
@ -828,7 +850,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence):
case let .peers(filter, isSelecting, _, _, filters, displayAutoremoveTimeout, displayPresence):
let itemPeer = peer.chatOrMonoforumMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
@ -868,7 +890,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _, _, _):
case let .peers(_, _, additionalCategories, _, _, _, _):
if !additionalCategories.isEmpty {
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
@ -1066,6 +1088,28 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
} : nil), directionHint: entry.directionHint)
case .HeaderEntry:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .TopPeer(_, 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, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch(isSavedMessages: false),
peer: .peer(peer: peer, chatPeer: peer),
status: .none,
requiresPremiumForMessaging: false,
enabled: true,
selection: .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
index: nil,
header: nil,
action: { _ in
nodeInteraction.peerSelected(peer, nil, nil, nil, false)
},
disabledAction: nil,
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader?
if case .peerType = mode {
@ -1091,8 +1135,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
}
}
private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade)
private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, presentationData: ChatListPresentationData, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries, presentationData: presentationData), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries, presentationData: presentationData), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade)
}
private final class ChatListOpaqueTransactionState {
@ -1196,7 +1240,7 @@ public final class ChatListNode: ListViewImpl {
return nil
case let .PeerId(value):
return PeerId(value)
case .ThreadId, .GroupId, .ContactId, .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .additionalCategory:
case .ThreadId, .GroupId, .ContactId, .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .additionalCategory, .TopPeer:
return nil
}
}
@ -1326,7 +1370,7 @@ public final class ChatListNode: ListViewImpl {
}
var isSelecting = false
if case .peers(_, true, _, _, _, _) = mode {
if case .peers(_, true, _, _, _, _, _) = mode {
isSelecting = true
}
@ -1839,7 +1883,7 @@ public final class ChatListNode: ListViewImpl {
let currentRemovingItemId = self.currentRemovingItemId
let savedMessagesPeer: Signal<EnginePeer?, NoError>
if case let .peers(filter, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil {
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
@ -2102,7 +2146,7 @@ public final class ChatListNode: ListViewImpl {
case .chatList:
isEmpty = false
return true
case let .peers(filter, _, _, _, _, _):
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 }
@ -2538,9 +2582,10 @@ public final class ChatListNode: ListViewImpl {
if accountIsPremium != previousAccountIsPremium.swap(accountIsPremium) {
forceAllUpdated = true
}
let presentationData = state.presentationData
return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated)
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) })
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, presentationData: presentationData, transition: $0) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
}
@ -3391,7 +3436,7 @@ public final class ChatListNode: ListViewImpl {
} else {
break loop
}
case .ArchiveIntro, .EmptyIntro, .SectionHeader, .HeaderEntry, .AdditionalCategory:
case .ArchiveIntro, .EmptyIntro, .SectionHeader, .HeaderEntry, .AdditionalCategory, .TopPeer:
break
}
}

View file

@ -18,6 +18,7 @@ enum ChatListNodeEntryId: Hashable {
case SectionHeader
case Notice
case additionalCategory(Int)
case TopPeer(EnginePeer.Id)
}
enum ChatListNodeEntrySortIndex: Comparable {
@ -25,6 +26,7 @@ enum ChatListNodeEntrySortIndex: Comparable {
case additionalCategory(Int)
case sectionHeader
case contact(id: EnginePeer.Id, presence: EnginePeer.Presence)
case topPeer(Int)
static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool {
switch lhs {
@ -34,6 +36,8 @@ enum ChatListNodeEntrySortIndex: Comparable {
return lhsIndex < rhsIndex
case .additionalCategory:
return false
case .topPeer:
return false
case .sectionHeader:
return true
case .contact:
@ -43,6 +47,8 @@ enum ChatListNodeEntrySortIndex: Comparable {
switch rhs {
case let .additionalCategory(rhsIndex):
return lhsIndex < rhsIndex
case .topPeer:
return true
case .index:
return true
case .sectionHeader:
@ -50,9 +56,18 @@ enum ChatListNodeEntrySortIndex: Comparable {
case .contact:
return true
}
case let .topPeer(lhsIndex):
switch rhs {
case .additionalCategory:
return false
case let .topPeer(rhsIndex):
return lhsIndex < rhsIndex
case .index, .sectionHeader, .contact:
return true
}
case .sectionHeader:
switch rhs {
case .additionalCategory, .index, .sectionHeader:
case .additionalCategory, .topPeer, .index, .sectionHeader:
return false
case .contact:
return true
@ -405,6 +420,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
case EmptyIntro(presentationData: ChatListPresentationData)
case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool)
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
case TopPeer(index: Int, peer: EnginePeer)
var sortIndex: ChatListNodeEntrySortIndex {
switch self {
@ -426,6 +442,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return .sectionHeader
case let .AdditionalCategory(index, _, _, _, _, _, _):
return .additionalCategory(index)
case let .TopPeer(index, _):
return .topPeer(index)
}
}
@ -454,6 +472,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
return .SectionHeader
case let .AdditionalCategory(_, id, _, _, _, _, _):
return .additionalCategory(id)
case let .TopPeer(_, peer):
return .TopPeer(peer.id)
}
}
@ -551,6 +571,12 @@ enum ChatListNodeEntry: Comparable, Identifiable {
} else {
return false
}
case let .TopPeer(index, peer):
if case .TopPeer(index, peer) = rhs {
return true
} else {
return false
}
}
}
}
@ -930,12 +956,18 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
}
if !view.hasLater {
if case let .peers(_, _, additionalCategories, _, _, _) = mode {
if case let .peers(_, _, additionalCategories, topPeers, _, _, _) = mode {
var index = 0
for category in additionalCategories.reversed() {
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
index += 1
}
index = 0
for topPeer in topPeers {
result.append(.TopPeer(index: index, peer: topPeer))
index += 1
}
} else if case let .peerType(types, hasCreate) = mode, !result.isEmpty && hasCreate {
for type in types {
switch type {

View file

@ -25,6 +25,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
private let filter: ChatListNodePeersFilter
private let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?
private let selectForumThreads: Bool
private let suggestedPeers: [EnginePeer]
private let attemptSelection: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)?
private let createNewGroup: (() -> Void)?
@ -114,6 +115,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.immediatelySwitchToContacts = params.immediatelySwitchToContacts
self.immediatelyActivateMultipleSelection = params.immediatelyActivateMultipleSelection
self.multipleSelectionLimit = params.multipleSelectionLimit
self.suggestedPeers = params.suggestedPeers
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
@ -263,7 +265,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
override public func loadDisplayNode() {
self.navigationBar?.secondaryContentHeight = 44.0 + 10.0
self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, suggestedPeers: self.suggestedPeers, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, presentInGlobalOverlay: { [weak self] c, a in
self?.presentInGlobalOverlay(c, with: a)

View file

@ -41,6 +41,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: [ReplyMarkupButtonRequestPeerType]?
private let suggestedPeers: [EnginePeer]
private var presentationInterfaceState: ChatPresentationInterfaceState
private let presentationInterfaceStatePromise = ValuePromise<ChatPresentationInterfaceState>()
@ -115,7 +116,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return (self.presentationData, self.presentationDataPromise.get())
}
init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, suggestedPeers: [EnginePeer], present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.controller = controller
self.present = present
@ -127,6 +128,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders
self.requestPeerType = requestPeerType
self.suggestedPeers = suggestedPeers
self.presentationData = presentationData
@ -209,7 +211,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
if let requestPeerType = self.requestPeerType {
chatListMode = .peerType(type: requestPeerType, hasCreate: hasCreation)
} else {
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false)
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, topPeers: self.suggestedPeers, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false)
}
if hasFilters {

View file

@ -46,6 +46,8 @@ swift_library(
"//submodules/SegmentedControlNode",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/HorizontalTabsComponent",
"//submodules/TelegramUI/Components/LiquidLens",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [

View file

@ -704,43 +704,46 @@ final class StorageFileListPanelComponent: Component {
private struct ItemLayout: Equatable {
let containerInsets: UIEdgeInsets
let containerWidth: CGFloat
let sideInset: CGFloat
let itemHeight: CGFloat
let itemCount: Int
let contentHeight: CGFloat
init(
containerInsets: UIEdgeInsets,
containerWidth: CGFloat,
sideInset: CGFloat,
itemHeight: CGFloat,
itemCount: Int
) {
self.containerInsets = containerInsets
self.containerWidth = containerWidth
self.sideInset = sideInset
self.itemHeight = itemHeight
self.itemCount = itemCount
self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = maxVisibleRow
if maxVisibleIndex >= minVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
func itemFrame(for index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight))
return CGRect(origin: CGPoint(x: self.sideInset, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth - self.sideInset * 2.0, height: self.itemHeight))
}
}
@ -752,37 +755,48 @@ final class StorageFileListPanelComponent: Component {
class View: UIView, UIScrollViewDelegate {
private let scrollView: ScrollViewImpl
private let listBackgroundView: UIImageView
private let listMaskView: UIImageView
private let measureItem = ComponentView<Empty>()
private var visibleItems: [EngineMessage.Id: ComponentView<Empty>] = [:]
private var ignoreScrolling: Bool = false
private var component: StorageFileListPanelComponent?
private var environment: StorageUsagePanelEnvironment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.scrollView = ScrollViewImpl()
self.listBackgroundView = UIImageView()
self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
self.listMaskView = UIImageView()
self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0)))
})?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate)
super.init(frame: frame)
self.addSubview(self.listBackgroundView)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
self.addSubview(self.listMaskView)
}
required init?(coder: NSCoder) {
@ -792,13 +806,44 @@ final class StorageFileListPanelComponent: Component {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
self.updateListBackground(transition: .immediate)
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
cancelContextGestures(view: scrollView)
}
private func updateListBackground(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout else {
return
}
let size = CGSize(width: itemLayout.containerWidth, height: self.scrollView.bounds.height)
guard size.width != 0.0 else {
return
}
let topInset = itemLayout.containerInsets.top
var distanceToTop: CGFloat = -100.0
var distanceToBottom: CGFloat = -100.0
let scrollingOffset = self.scrollView.contentOffset.y
distanceToTop = -scrollingOffset + topInset
let itemsContentHeight = self.scrollView.contentSize.height - itemLayout.containerInsets.bottom
let contentBottomOffset = itemsContentHeight - scrollingOffset - self.scrollView.bounds.height
distanceToBottom = -contentBottomOffset
distanceToTop = max(-100.0, distanceToTop)
distanceToBottom = max(-100.0, distanceToBottom)
let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop)))
let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height))
transition.setFrame(view: self.listBackgroundView, frame: listBackgroundFrame)
transition.setFrame(view: self.listMaskView, frame: listMaskFrame)
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
return
@ -975,7 +1020,7 @@ final class StorageFileListPanelComponent: Component {
contextAction: component.contextAction
)),
environment: {},
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.sideInset * 2.0, height: itemLayout.itemHeight)
)
let itemFrame = itemLayout.itemFrame(for: index)
if let itemComponentView = itemView.view {
@ -1008,6 +1053,9 @@ final class StorageFileListPanelComponent: Component {
let environment = environment[StorageUsagePanelEnvironment.self].value
self.environment = environment
self.listBackgroundView.tintColor = environment.theme.list.itemBlocksBackgroundColor
self.listMaskView.tintColor = environment.theme.list.blocksBackgroundColor
let measureItemSize = self.measureItem.update(
transition: .immediate,
@ -1034,6 +1082,7 @@ final class StorageFileListPanelComponent: Component {
let itemLayout = ItemLayout(
containerInsets: environment.containerInsets,
containerWidth: availableSize.width,
sideInset: 16.0,
itemHeight: measureItemSize.height,
itemCount: component.items?.items.count ?? 0
)
@ -1060,7 +1109,8 @@ final class StorageFileListPanelComponent: Component {
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
self.updateListBackground(transition: transition)
return availableSize
}
}

View file

@ -454,43 +454,46 @@ final class StoragePeerListPanelComponent: Component {
private struct ItemLayout: Equatable {
let containerInsets: UIEdgeInsets
let containerWidth: CGFloat
let sideInset: CGFloat
let itemHeight: CGFloat
let itemCount: Int
let contentHeight: CGFloat
init(
containerInsets: UIEdgeInsets,
containerWidth: CGFloat,
sideInset: CGFloat,
itemHeight: CGFloat,
itemCount: Int
) {
self.containerInsets = containerInsets
self.containerWidth = containerWidth
self.sideInset = sideInset
self.itemHeight = itemHeight
self.itemCount = itemCount
self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = maxVisibleRow
if maxVisibleIndex >= minVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
func itemFrame(for index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight))
return CGRect(origin: CGPoint(x: self.sideInset, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth - self.sideInset * 2.0, height: self.itemHeight))
}
}
@ -502,7 +505,9 @@ final class StoragePeerListPanelComponent: Component {
class View: UIView, UIScrollViewDelegate {
private let scrollView: ScrollViewImpl
private let listBackgroundView: UIImageView
private let listMaskView: UIImageView
private let measureItem = ComponentView<Empty>()
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
@ -514,25 +519,35 @@ final class StoragePeerListPanelComponent: Component {
override init(frame: CGRect) {
self.scrollView = ScrollViewImpl()
self.listBackgroundView = UIImageView()
self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
self.listMaskView = UIImageView()
self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0)))
})?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate)
super.init(frame: frame)
self.addSubview(self.listBackgroundView)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
self.addSubview(self.listMaskView)
}
required init?(coder: NSCoder) {
@ -542,13 +557,44 @@ final class StoragePeerListPanelComponent: Component {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
self.updateListBackground(transition: .immediate)
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
cancelContextGestures(view: scrollView)
}
private func updateListBackground(transition: ComponentTransition) {
guard let itemLayout = self.itemLayout else {
return
}
let size = CGSize(width: itemLayout.containerWidth, height: self.scrollView.bounds.height)
guard size.width != 0.0 else {
return
}
let topInset = itemLayout.containerInsets.top
var distanceToTop: CGFloat = -100.0
var distanceToBottom: CGFloat = -100.0
let scrollingOffset = self.scrollView.contentOffset.y
distanceToTop = -scrollingOffset + topInset
let itemsContentHeight = self.scrollView.contentSize.height - itemLayout.containerInsets.bottom
let contentBottomOffset = itemsContentHeight - scrollingOffset - self.scrollView.bounds.height
distanceToBottom = -contentBottomOffset
distanceToTop = max(-100.0, distanceToTop)
distanceToBottom = max(-100.0, distanceToBottom)
let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop)))
let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height))
transition.setFrame(view: self.listBackgroundView, frame: listBackgroundFrame)
transition.setFrame(view: self.listMaskView, frame: listMaskFrame)
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
return
@ -607,7 +653,7 @@ final class StoragePeerListPanelComponent: Component {
contextAction: component.contextAction
)),
environment: {},
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.sideInset * 2.0, height: itemLayout.itemHeight)
)
let itemFrame = itemLayout.itemFrame(for: index)
if let itemComponentView = itemView.view {
@ -640,6 +686,9 @@ final class StoragePeerListPanelComponent: Component {
let environment = environment[StorageUsagePanelEnvironment.self].value
self.environment = environment
self.listBackgroundView.tintColor = environment.theme.list.itemBlocksBackgroundColor
self.listMaskView.tintColor = environment.theme.list.blocksBackgroundColor
let measureItemSize = self.measureItem.update(
transition: .immediate,
@ -664,6 +713,7 @@ final class StoragePeerListPanelComponent: Component {
let itemLayout = ItemLayout(
containerInsets: environment.containerInsets,
containerWidth: availableSize.width,
sideInset: 16.0,
itemHeight: measureItemSize.height,
itemCount: component.items?.items.count ?? 0
)
@ -690,7 +740,8 @@ final class StoragePeerListPanelComponent: Component {
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
self.updateListBackground(transition: transition)
return availableSize
}
}

View file

@ -4,6 +4,8 @@ import Display
import ComponentFlow
import ComponentDisplayAdapters
import TelegramPresentationData
import HorizontalTabsComponent
import GlassBackgroundComponent
final class StorageUsagePanelContainerEnvironment: Equatable {
let isScrollable: Bool
@ -63,264 +65,6 @@ final class StorageUsagePanelEnvironment: Equatable {
}
}
private final class StorageUsageHeaderItemComponent: CombinedComponent {
let theme: PresentationTheme
let title: String
let activityFraction: CGFloat
init(
theme: PresentationTheme,
title: String,
activityFraction: CGFloat
) {
self.theme = theme
self.title = title
self.activityFraction = activityFraction
}
static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.activityFraction != rhs.activityFraction {
return false
}
return true
}
static var body: Body {
let activeText = Child(Text.self)
let inactiveText = Child(Text.self)
return { context in
let activeText = activeText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: .immediate
)
let inactiveText = inactiveText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor),
availableSize: context.availableSize,
transition: .immediate
)
context.add(activeText
.position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5))
.opacity(context.component.activityFraction)
)
context.add(inactiveText
.position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5))
.opacity(1.0 - context.component.activityFraction)
)
return activeText.size
}
}
}
private final class StorageUsageHeaderComponent: Component {
struct Item: Equatable {
let id: AnyHashable
let title: String
init(
id: AnyHashable,
title: String
) {
self.id = id
self.title = title
}
}
let theme: PresentationTheme
let items: [Item]
let activeIndex: Int
let transitionFraction: CGFloat
let switchToPanel: (AnyHashable) -> Void
init(
theme: PresentationTheme,
items: [Item],
activeIndex: Int,
transitionFraction: CGFloat,
switchToPanel: @escaping (AnyHashable) -> Void
) {
self.theme = theme
self.items = items
self.activeIndex = activeIndex
self.transitionFraction = transitionFraction
self.switchToPanel = switchToPanel
}
static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.activeIndex != rhs.activeIndex {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
return false
}
return true
}
class View: UIView {
private var component: StorageUsageHeaderComponent?
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
private let activeItemLayer: SimpleLayer
override init(frame: CGRect) {
self.activeItemLayer = SimpleLayer()
self.activeItemLayer.cornerRadius = 2.0
self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
super.init(frame: frame)
self.layer.addSublayer(self.activeItemLayer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
var closestId: (CGFloat, AnyHashable)?
if self.bounds.contains(point) {
for (id, item) in self.visibleItems {
if let itemView = item.view {
let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX))
if let closestIdValue = closestId {
if distance < closestIdValue.0 {
closestId = (distance, id)
}
} else {
closestId = (distance, id)
}
}
}
}
if let closestId = closestId, let component = self.component {
component.switchToPanel(closestId.1)
}
}
}
func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
var validIds = Set<AnyHashable>()
for i in 0 ..< component.items.count {
let item = component.items[i]
validIds.insert(item.id)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.visibleItems[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[item.id] = itemView
}
let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction
let activityDistance: CGFloat = abs(activeIndex - CGFloat(i))
let activityFraction: CGFloat
if activityDistance < 1.0 {
activityFraction = 1.0 - activityDistance
} else {
activityFraction = 0.0
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StorageUsageHeaderItemComponent(
theme: component.theme,
title: item.title,
activityFraction: activityFraction
)),
environment: {},
containerSize: availableSize
)
let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count)
let itemX: CGFloat
if component.items.count == 1 {
itemX = 37.0
} else {
itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0)
}
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
itemComponentView.isUserInteractionEnabled = false
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
if component.activeIndex < component.items.count {
let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view
let nextIndex: Int
if component.transitionFraction > 0.0 {
nextIndex = max(0, component.activeIndex - 1)
} else {
nextIndex = min(component.items.count - 1, component.activeIndex + 1)
}
let nextView = self.visibleItems[component.items[nextIndex].id]?.view
if let activeView = activeView, let nextView = nextView {
let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction))
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0)))
}
}
if themeUpdated {
self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class StorageUsagePanelContainerComponent: Component {
typealias EnvironmentType = StorageUsagePanelContainerEnvironment
@ -383,10 +127,9 @@ final class StorageUsagePanelContainerComponent: Component {
}
class View: UIView, UIGestureRecognizerDelegate {
private let topPanelBackgroundView: UIView
private let topPanelMergedBackgroundView: UIView
private let topPanelSeparatorLayer: SimpleLayer
private let header = ComponentView<Empty>()
private let tabsBackgroundContainer: GlassBackgroundContainerView
private let tabsBackgroundView: GlassBackgroundView
private let tabsContainer = ComponentView<Empty>()
private var component: StorageUsagePanelContainerComponent?
private weak var state: EmptyComponentState?
@ -396,25 +139,21 @@ final class StorageUsagePanelContainerComponent: Component {
private var actualVisibleIds = Set<AnyHashable>()
private var currentId: AnyHashable?
private var transitionFraction: CGFloat = 0.0
private var isDraggingTabs: Bool = false
private var animatingTransition: Bool = false
override init(frame: CGRect) {
self.topPanelBackgroundView = UIView()
self.topPanelMergedBackgroundView = UIView()
self.topPanelMergedBackgroundView.alpha = 0.0
self.topPanelSeparatorLayer = SimpleLayer()
self.tabsBackgroundContainer = GlassBackgroundContainerView()
self.tabsBackgroundView = GlassBackgroundView()
self.panelsBackgroundLayer = SimpleLayer()
super.init(frame: frame)
self.layer.addSublayer(self.panelsBackgroundLayer)
self.addSubview(self.topPanelBackgroundView)
self.addSubview(self.topPanelMergedBackgroundView)
self.layer.addSublayer(self.topPanelSeparatorLayer)
self.tabsBackgroundContainer.contentView.addSubview(self.tabsBackgroundView)
self.addSubview(self.tabsBackgroundContainer)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let self, let component = self.component, let currentId = self.currentId else {
return []
@ -422,11 +161,7 @@ final class StorageUsagePanelContainerComponent: Component {
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return []
}
/*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
return []
}*/
if index == 0 {
return .left
}
@ -480,7 +215,8 @@ final class StorageUsagePanelContainerComponent: Component {
}
cancelContextGestures(view: self)
self.isDraggingTabs = true
//self.animatingTransition = true
case .changed:
guard let component = self.component, let currentId = self.currentId else {
@ -535,6 +271,7 @@ final class StorageUsagePanelContainerComponent: Component {
component.currentPanelUpdated(currentId, transition)
}
self.isDraggingTabs = false
self.animatingTransition = false
//self.currentPaneUpdated?(false)
@ -545,8 +282,16 @@ final class StorageUsagePanelContainerComponent: Component {
}
func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) {
transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value)
transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else {
return nil
}
if point.y < component.insets.top {
return nil
}
return super.hitTest(point, with: event)
}
func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: ComponentTransition) -> CGSize {
@ -558,34 +303,30 @@ final class StorageUsagePanelContainerComponent: Component {
self.state = state
if themeUpdated {
self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.topPanelMergedBackgroundView.backgroundColor = component.theme.list.blocksBackgroundColor
//self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
}
let topPanelCoverHeight: CGFloat = 10.0
let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0))
transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame)
transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame)
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)))
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
let tabsHeight: CGFloat = 40.0
let tabsTopInset: CGFloat = component.insets.top + 10.0
let tabsBottomInset: CGFloat = 10.0
let tabsSideInset: CGFloat = 16.0 + component.insets.left
let tabsContainerSize = CGSize(width: availableSize.width - tabsSideInset * 2.0, height: tabsHeight)
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: component.insets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.insets.top)))
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
self.currentId = nil
}
if self.currentId == nil {
self.currentId = component.items.first?.id
}
var visibleIds = Set<AnyHashable>()
var currentIndex: Int?
if let currentId = self.currentId {
visibleIds.insert(currentId)
if let index = component.items.firstIndex(where: { $0.id == currentId }) {
currentIndex = index
if index != 0 {
@ -596,50 +337,73 @@ final class StorageUsagePanelContainerComponent: Component {
}
}
}
let _ = self.header.update(
let tabsContainerEffectiveSize = self.tabsContainer.update(
transition: transition,
component: AnyComponent(StorageUsageHeaderComponent(
component: AnyComponent(HorizontalTabsComponent(
context: nil,
theme: component.theme,
items: component.items.map { item -> StorageUsageHeaderComponent.Item in
return StorageUsageHeaderComponent.Item(
tabs: component.items.map { item -> HorizontalTabsComponent.Tab in
return HorizontalTabsComponent.Tab(
id: item.id,
title: item.title
content: .title(HorizontalTabsComponent.Tab.Title(text: item.title, entities: [], enableAnimations: false)),
badge: nil,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
if component.items.contains(where: { $0.id == item.id }) {
self.currentId = item.id
let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))
self.state?.updated(transition: transition)
component.currentPanelUpdated(item.id, transition)
}
}
)
},
activeIndex: currentIndex ?? 0,
transitionFraction: self.transitionFraction,
switchToPanel: { [weak self] id in
guard let self, let component = self.component else {
return
}
if component.items.contains(where: { $0.id == id }) {
self.currentId = id
let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))
self.state?.updated(transition: transition)
component.currentPanelUpdated(id, transition)
}
}
selectedTab: self.currentId,
isEditing: false,
layout: .fit,
liftWhileSwitching: true
)),
environment: {},
containerSize: topPanelFrame.size
containerSize: tabsContainerSize
)
if let headerView = self.header.view {
if headerView.superview == nil {
self.addSubview(headerView)
let tabContainerFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - tabsContainerEffectiveSize.width) / 2.0),
y: tabsTopInset
),
size: tabsContainerEffectiveSize
)
transition.setFrame(view: self.tabsBackgroundContainer, frame: tabContainerFrame)
self.tabsBackgroundContainer.update(size: tabContainerFrame.size, isDark: component.theme.overallDarkAppearance, transition: transition)
transition.setFrame(view: self.tabsBackgroundView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size))
self.tabsBackgroundView.update(size: tabContainerFrame.size, cornerRadius: tabContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition)
if let tabsContainerView = self.tabsContainer.view as? HorizontalTabsComponent.View {
if tabsContainerView.superview == nil {
self.tabsBackgroundView.contentView.addSubview(tabsContainerView)
tabsContainerView.setOverlayContainerView(overlayContainerView: self)
}
transition.setFrame(view: headerView, frame: topPanelFrame)
transition.setFrame(view: tabsContainerView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size))
tabsContainerView.updateTabSwitchFraction(fraction: self.transitionFraction, isDragging: self.isDraggingTabs, transition: transition)
}
let effectiveTabsHeight = tabsTopInset + tabContainerFrame.height + tabsBottomInset
let childEnvironment = StorageUsagePanelEnvironment(
theme: component.theme,
strings: component.strings,
dateTimeFormat: component.dateTimeFormat,
containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
containerInsets: UIEdgeInsets(top: effectiveTabsHeight, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
isScrollable: environment.isScrollable
)
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))
if self.animatingTransition {
visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil })
@ -721,7 +485,7 @@ final class StorageUsagePanelContainerComponent: Component {
)
if let panelView = panel.view {
if panelView.superview == nil {
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
self.insertSubview(panelView, belowSubview: self.tabsBackgroundContainer)
}
panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in

View file

@ -1122,8 +1122,8 @@ final class StorageUsageScreenComponent: Component {
}
var wasLockedAtPanels = false
if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics {
if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel {
if let panelContainerView = self.panelContainer.view {
if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - 0.0)) <= UIScreenPixel {
wasLockedAtPanels = true
}
}
@ -2214,7 +2214,7 @@ final class StorageUsageScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
dateTimeFormat: environment.dateTimeFormat,
insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right),
insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right),
items: panelItems,
currentPanelUpdated: { [weak self] id, transition in
guard let self else {
@ -2227,20 +2227,20 @@ final class StorageUsageScreenComponent: Component {
environment: {
StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels)
},
containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
if let panelContainerView = self.panelContainer.view {
if panelContainerView.superview == nil {
self.scrollContainerView.addSubview(panelContainerView)
}
transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize))
transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - environment.navigationHeight), size: panelContainerSize))
if self.topContentOverlayView.superview == nil {
self.scrollContainerView.insertSubview(self.topContentOverlayView, belowSubview: panelContainerView)
}
self.topContentOverlayView.backgroundColor = environment.theme.list.blocksBackgroundColor
transition.setFrame(view: self.topContentOverlayView, frame: CGRect(origin: CGPoint(x: 0.0, y: panelContainerView.frame.minY - availableSize.height), size: availableSize))
}
contentHeight += panelContainerSize.height
contentHeight += panelContainerSize.height - environment.navigationHeight
} else {
self.panelContainer.view?.removeFromSuperview()
self.topContentOverlayView.removeFromSuperview()
@ -2259,7 +2259,7 @@ final class StorageUsageScreenComponent: Component {
var scrollViewBounds = self.scrollView.bounds
scrollViewBounds.size = availableSize
if wasLockedAtPanels, let panelContainerView = self.panelContainer.view {
scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight
scrollViewBounds.origin.y = panelContainerView.frame.minY
}
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)

View file

@ -554,85 +554,96 @@ func presentChatReplyOptions(selfController: ChatControllerImpl, sourceView: UIV
func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) {
let _ = selfController.presentVoiceMessageDiscardAlert(action: { [weak selfController] in
guard let selfController else {
return
}
let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages]
var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?
let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(
context: selfController.context,
updatedPresentationData: selfController.updatedPresentationData,
filter: filter,
hasFilters: true,
title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle,
attemptSelection: { peer, _, reason in
attemptSelectionImpl?(peer, reason)
},
multipleSelection: false,
forwardedMessageIds: [],
selectForumThreads: true
))
let context = selfController.context
attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in
guard let selfController, let controller = controller else {
Task { @MainActor in
guard let selfController else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch reason {
case .generic:
controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
case .premiumRequired:
controller.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages]
var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?
var suggestedPeers: [EnginePeer] = []
if let message = await selfController.context.engine.data.get(
TelegramEngine.EngineData.Item.Messages.Message(id: replySubject.messageId)
).get(), case let .user(author) = message.author {
suggestedPeers.append(.user(author))
}
let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(
context: selfController.context,
updatedPresentationData: selfController.updatedPresentationData,
filter: filter,
hasFilters: true,
title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle,
attemptSelection: { peer, _, reason in
attemptSelectionImpl?(peer, reason)
},
multipleSelection: false,
forwardedMessageIds: [],
selectForumThreads: true,
suggestedPeers: suggestedPeers
))
let context = selfController.context
attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in
guard let selfController, let controller = controller else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch reason {
case .generic:
controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
case .premiumRequired:
controller.forEachController { c in
if let c = c as? UndoOverlayController {
c.dismiss()
}
return true
}
return true
}
var hasAction = false
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: selfController.context.currentAppConfiguration.with { $0 })
if !premiumConfiguration.isPremiumDisabled {
hasAction = true
}
controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: hasAction ? presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action : nil, timeout: nil, linkAction: { _ in
}), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in
guard let selfController, let controller else {
var hasAction = false
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: selfController.context.currentAppConfiguration.with { $0 })
if !premiumConfiguration.isPremiumDisabled {
hasAction = true
}
controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: hasAction ? presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action : nil, timeout: nil, linkAction: { _ in
}), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in
guard let selfController, let controller else {
return false
}
if case .undo = action {
let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings)
controller.push(premiumController)
}
return false
}
if case .undo = action {
let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings)
controller.push(premiumController)
}
return false
}), in: .current)
}), in: .current)
}
}
controller.peerSelected = { [weak selfController, weak controller] peer, threadId in
guard let selfController, let strongController = controller else {
return
}
let peerId = peer.id
//let accountPeerId = selfController.context.account.peerId
var isPinnedMessages = false
if case .pinnedMessages = selfController.presentationInterfaceState.subject {
isPinnedMessages = true
}
if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages {
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
selfController.updateItemNodesSearchTextHighlightStates()
selfController.searchResultsController = nil
strongController.dismiss()
} else {
moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in
strongController?.dismiss()
})
}
}
selfController.chatDisplayNode.dismissInput()
selfController.effectiveNavigationController?.pushViewController(controller)
}
controller.peerSelected = { [weak selfController, weak controller] peer, threadId in
guard let selfController, let strongController = controller else {
return
}
let peerId = peer.id
//let accountPeerId = selfController.context.account.peerId
var isPinnedMessages = false
if case .pinnedMessages = selfController.presentationInterfaceState.subject {
isPinnedMessages = true
}
if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages {
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
selfController.updateItemNodesSearchTextHighlightStates()
selfController.searchResultsController = nil
strongController.dismiss()
} else {
moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in
strongController?.dismiss()
})
}
}
selfController.chatDisplayNode.dismissInput()
selfController.effectiveNavigationController?.pushViewController(controller)
})
}

View file

@ -189,7 +189,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
}
placeholder = placeholderValue
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], topPeers: [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
chatListNode.passthroughPeerSelection = true
chatListNode.disabledPeerSelected = { peer, _, reason in
attemptDisabledItemSelection?(peer, reason)