Add empty search result chat list footer

This commit is contained in:
Isaac 2024-12-28 02:43:55 +08:00
parent a5579103f4
commit dea8f6b48c
7 changed files with 377 additions and 12 deletions

View File

@ -593,8 +593,10 @@ private struct NotificationContent: CustomStringConvertible {
if !self.userInfo.isEmpty { if !self.userInfo.isEmpty {
content.userInfo = self.userInfo content.userInfo = self.userInfo
} }
if !self.attachments.isEmpty { if self.isLockedMessage == nil {
content.attachments = self.attachments if !self.attachments.isEmpty {
content.attachments = self.attachments
}
} }
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {

View File

@ -13576,3 +13576,8 @@ Sorry for the inconvenience.";
"Notification.StarGift.Subtitle.Downgraded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned."; "Notification.StarGift.Subtitle.Downgraded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.";
"Gift.View.KeepOrUpgradeDescription" = "You can keep this gift or upgrade it."; "Gift.View.KeepOrUpgradeDescription" = "You can keep this gift or upgrade it.";
"VideoChat.IncomingVideoQuality.AudioOnly" = "Audio Only";
"VideoChat.IncomingVideoQuality.Title" = "Receive Video Quality";
"ChatList.EmptyResult.SearchInAll" = "Search in All Messages";

View File

@ -111,6 +111,7 @@ swift_library(
"//submodules/ComposePollUI", "//submodules/ComposePollUI",
"//submodules/ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState",
"//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUI/Components/LottieComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -380,6 +380,7 @@ public enum ChatListSearchEntryStableId: Hashable {
case globalPeerId(EnginePeer.Id) case globalPeerId(EnginePeer.Id)
case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection) case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
case messagePlaceholder(Int32) case messagePlaceholder(Int32)
case emptyMessagesFooter
case addContact case addContact
} }
@ -442,6 +443,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?) case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool, TelegramSearchPeersScope) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool, TelegramSearchPeersScope)
case messagePlaceholder(Int32, ChatListPresentationData, TelegramSearchPeersScope) case messagePlaceholder(Int32, ChatListPresentationData, TelegramSearchPeersScope)
case emptyMessagesFooter(ChatListPresentationData, TelegramSearchPeersScope, String?)
case addContact(String, PresentationTheme, PresentationStrings) case addContact(String, PresentationTheme, PresentationStrings)
public var stableId: ChatListSearchEntryStableId { public var stableId: ChatListSearchEntryStableId {
@ -458,6 +460,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .messageId(message.id, section) return .messageId(message.id, section)
case let .messagePlaceholder(index, _, _): case let .messagePlaceholder(index, _, _):
return .messagePlaceholder(index) return .messagePlaceholder(index)
case .emptyMessagesFooter:
return .emptyMessagesFooter
case .addContact: case .addContact:
return .addContact return .addContact
} }
@ -561,6 +565,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .emptyMessagesFooter(lhsPresentationData, lhsSearchScope, lhsQuery):
if case let .emptyMessagesFooter(rhsPresentationData, rhsSearchScope, rhsQuery) = rhs {
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsSearchScope != rhsSearchScope {
return false
}
if lhsQuery != rhsQuery {
return false
}
return true
} else {
return false
}
case let .addContact(lhsPhoneNumber, lhsTheme, lhsStrings): case let .addContact(lhsPhoneNumber, lhsTheme, lhsStrings):
if case let .addContact(rhsPhoneNumber, rhsTheme, rhsStrings) = rhs { if case let .addContact(rhsPhoneNumber, rhsTheme, rhsStrings) = rhs {
if lhsPhoneNumber != rhsPhoneNumber { if lhsPhoneNumber != rhsPhoneNumber {
@ -601,7 +620,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return false return false
case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _, _, _): case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
case .globalPeer, .message, .messagePlaceholder, .addContact: case .globalPeer, .message, .messagePlaceholder, .emptyMessagesFooter, .addContact:
return true return true
} }
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _): case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
@ -610,7 +629,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return false return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _): case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
case .message, .messagePlaceholder, .addContact: case .message, .messagePlaceholder, .emptyMessagesFooter, .addContact:
return true return true
} }
case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _, _, _, _): case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _, _, _, _):
@ -618,6 +637,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return lhsKey < rhsKey return lhsKey < rhsKey
} else if case .messagePlaceholder = rhs { } else if case .messagePlaceholder = rhs {
return true return true
} else if case .emptyMessagesFooter = rhs {
return true
} else if case .addContact = rhs { } else if case .addContact = rhs {
return true return true
} else { } else {
@ -626,11 +647,19 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case let .messagePlaceholder(lhsIndex, _, _): case let .messagePlaceholder(lhsIndex, _, _):
if case let .messagePlaceholder(rhsIndex, _, _) = rhs { if case let .messagePlaceholder(rhsIndex, _, _) = rhs {
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} else if case .emptyMessagesFooter = rhs {
return true
} else if case .addContact = rhs { } else if case .addContact = rhs {
return true return true
} else { } else {
return false return false
} }
case .emptyMessagesFooter:
if case .addContact = rhs {
return true
} else {
return false
}
case .addContact: case .addContact:
return false return false
} }
@ -658,7 +687,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
toggleAllPaused: @escaping () -> Void, toggleAllPaused: @escaping () -> Void,
openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void,
openPublicPosts: @escaping () -> Void, openPublicPosts: @escaping () -> Void,
openMessagesFilter: @escaping (ASDisplayNode) -> Void openMessagesFilter: @escaping (ASDisplayNode) -> Void,
switchMessagesFilter: @escaping (TelegramSearchPeersScope) -> Void
) -> ListViewItem { ) -> ListViewItem {
switch self { switch self {
case let .topic(peer, threadInfo, _, theme, strings, expandType): case let .topic(peer, threadInfo, _, theme, strings, expandType):
@ -1128,6 +1158,33 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openMessagesFilter(sourceNode) openMessagesFilter(sourceNode)
}) })
return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0))), content: .loading, editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction) return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0))), content: .loading, editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction)
case let .emptyMessagesFooter(presentationData, searchScope, searchQuery):
var actionTitle: String?
let filterTitle: String
switch searchScope {
case .everywhere:
filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats
case .channels:
filterTitle = presentationData.strings.ChatList_Search_Messages_Channels
case .groups:
filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats
case .privateChats:
filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats
}
actionTitle = "\(filterTitle) <"
let header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in
openMessagesFilter(sourceNode)
})
return ChatListSearchEmptyFooterItem(
theme: presentationData.theme,
strings: presentationData.strings,
header: header,
searchQuery: searchQuery,
searchAllMessages: searchScope == .everywhere ? nil : {
switchMessagesFilter(.everywhere)
}
)
case let .addContact(phoneNumber, theme, strings): case let .addContact(phoneNumber, theme, strings):
return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
interaction.addContact(phoneNumber) interaction.addContact(phoneNumber)
@ -1201,12 +1258,42 @@ private func chatListSearchContainerPreparedRecentTransition(
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty) return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty)
} }
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void, openMessagesFilter: @escaping (ASDisplayNode) -> Void) -> ChatListSearchContainerTransition { public func chatListSearchContainerPreparedTransition(
from fromEntries: [ChatListSearchEntry],
to toEntries: [ChatListSearchEntry],
displayingResults: Bool,
isEmpty: Bool,
isLoading: Bool,
animated: Bool,
context: AccountContext,
presentationData: PresentationData,
enableHeaders: Bool,
filter: ChatListNodePeersFilter,
requestPeerType: [ReplyMarkupButtonRequestPeerType]?,
location: ChatListControllerLocation,
key: ChatListSearchPaneKey,
tagMask: EngineMessage.Tags?,
interaction: ChatListNodeInteraction,
listInteraction: ListMessageItemInteraction,
peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?,
toggleExpandLocalResults: @escaping () -> Void,
toggleExpandGlobalResults: @escaping () -> Void,
searchPeer: @escaping (EnginePeer) -> Void,
searchQuery: String?,
searchOptions: ChatListSearchOptions?,
messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?,
openClearRecentlyDownloaded: @escaping () -> Void,
toggleAllPaused: @escaping () -> Void,
openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void,
openPublicPosts: @escaping () -> Void,
openMessagesFilter: @escaping (ASDisplayNode) -> Void,
switchMessagesFilter: @escaping (TelegramSearchPeersScope) -> Void
) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter), directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
} }
@ -2912,6 +2999,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
index += 1 index += 1
} }
} else { } else {
var hasAnyMessages = false
for foundRemoteMessageSet in foundRemoteMessages.0 { for foundRemoteMessageSet in foundRemoteMessages.0 {
for message in foundRemoteMessageSet.messages { for message in foundRemoteMessageSet.messages {
if existingMessageIds.contains(message.id) { if existingMessageIds.contains(message.id) {
@ -2936,10 +3024,22 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} }
//TODO:requiresPremiumForMessaging //TODO:requiresPremiumForMessaging
hasAnyMessages = true
entries.append(.message(message, peer, foundRemoteMessageSet.readCounters[message.id.peerId], foundRemoteMessageSet.threadsData[message.id]?.info, presentationData, foundRemoteMessageSet.totalCount, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false, nil, false, searchScope)) entries.append(.message(message, peer, foundRemoteMessageSet.readCounters[message.id.peerId], foundRemoteMessageSet.threadsData[message.id]?.info, presentationData, foundRemoteMessageSet.totalCount, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false, nil, false, searchScope))
index += 1 index += 1
} }
} }
if !hasAnyMessages {
switch searchScope {
case .everywhere:
break
default:
if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_empty_search_footer"] != nil {
} else {
entries.append(.emptyMessagesFooter(presentationData, searchScope, query))
}
}
}
} }
} }
@ -3444,6 +3544,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.interaction.switchToFilter(.publicPosts) strongSelf.interaction.switchToFilter(.publicPosts)
}, openMessagesFilter: { sourceNode in }, openMessagesFilter: { sourceNode in
strongSelf.openMessagesFilter(sourceNode: sourceNode) strongSelf.openMessagesFilter(sourceNode: sourceNode)
}, switchMessagesFilter: { filter in
strongSelf.searchScopePromise.set(.everywhere)
}) })
strongSelf.currentEntries = newEntries strongSelf.currentEntries = newEntries
if strongSelf.key == .downloads { if strongSelf.key == .downloads {

View File

@ -4,6 +4,8 @@ import AsyncDisplayKit
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import ComponentFlow
import LottieComponent
class ChatListHoleItem: ListViewItem { class ChatListHoleItem: ListViewItem {
let theme: PresentationTheme let theme: PresentationTheme
@ -78,3 +80,252 @@ class ChatListHoleItemNode: ListViewItemNode {
} }
} }
} }
class ChatListSearchEmptyFooterItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let searchQuery: String?
let searchAllMessages: (() -> Void)?
let header: ListViewItemHeader?
let selectable: Bool = false
init(theme: PresentationTheme, strings: PresentationStrings, header: ListViewItemHeader?, searchQuery: String?, searchAllMessages: (() -> Void)?) {
self.theme = theme
self.strings = strings
self.header = header
self.searchQuery = searchQuery
self.searchAllMessages = searchAllMessages
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatListSearchEmptyFooterItemNode()
let (layout, apply) = node.asyncLayout()(self, params)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListSearchEmptyFooterItemNode)
if let nodeValue = node() as? ChatListSearchEmptyFooterItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListSearchEmptyFooterItemNode: ListViewItemNode {
private let contentNode: ASDisplayNode
private let titleNode: TextNode
private let textNode: TextNode
private let searchAllMessagesButton: HighlightableButtonNode
private let searchAllMessagesTitle: TextNode
private let icon = ComponentView<Empty>()
private var item: ChatListSearchEmptyFooterItem?
required init() {
self.contentNode = ASDisplayNode()
self.titleNode = TextNode()
self.textNode = TextNode()
self.searchAllMessagesButton = HighlightableButtonNode()
self.searchAllMessagesTitle = TextNode()
self.searchAllMessagesTitle.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.titleNode)
self.contentNode.addSubnode(self.textNode)
self.contentNode.addSubnode(self.searchAllMessagesButton)
self.searchAllMessagesButton.addSubnode(self.searchAllMessagesTitle)
self.searchAllMessagesButton.addTarget(self, action: #selector(self.searchAllMessagesButtonPressed), forControlEvents: .touchUpInside)
self.wantsTrailingItemSpaceUpdates = true
}
@objc private func searchAllMessagesButtonPressed() {
self.item?.searchAllMessages?()
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListSearchEmptyFooterItem, params)
apply()
}
override func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
override func updateTrailingItemSpace(_ trailingItemSpace: CGFloat, transition: ContainedViewLayoutTransition) {
var contentFrame = self.contentNode.frame
contentFrame.origin.y = max(0.0, floor(trailingItemSpace * 0.5))
self.contentNode.frame = contentFrame
}
func asyncLayout() -> (_ item: ChatListSearchEmptyFooterItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleNodeLayout = TextNode.asyncLayout(self.titleNode)
let makeTextNodeLayout = TextNode.asyncLayout(self.textNode)
let makeSearchAllMessagesTitleLayout = TextNode.asyncLayout(self.searchAllMessagesTitle)
return { [weak self] item, params in
let titleLayout = makeTitleNodeLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: item.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: item.theme.list.freeTextColor),
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0)
))
let textValue: String
if let searchQuery = item.searchQuery {
textValue = item.strings.ChatList_Search_NoResultsQueryDescription(searchQuery).string
} else {
textValue = item.strings.ChatList_Search_NoResults
}
let textLayout = makeTextNodeLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: textValue, font: Font.regular(16.0), textColor: item.theme.list.freeTextColor),
maximumNumberOfLines: 0,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0),
alignment: .center,
lineSpacing: 0.1
))
let searchAllMessagesTitleLayout = makeSearchAllMessagesTitleLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: item.strings.ChatList_EmptyResult_SearchInAll, font: Font.regular(17.0), textColor: item.theme.list.itemAccentColor),
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0)
))
var contentHeight: CGFloat = 0.0
let topInset: CGFloat = 40.0
let bottomInset: CGFloat = 10.0
let iconSpacing: CGFloat = 20.0
let titleSpacing: CGFloat = 6.0
let buttonSpacing: CGFloat = 14.0
let buttonInset: CGFloat = 11.0
let iconSize = CGSize(width: 128.0, height: 128.0)
contentHeight += topInset
contentHeight += iconSize.height
contentHeight += iconSpacing
contentHeight += titleLayout.0.size.height
contentHeight += titleSpacing
contentHeight += textLayout.0.size.height
if item.searchAllMessages != nil {
contentHeight += buttonSpacing
contentHeight += buttonInset
contentHeight += searchAllMessagesTitleLayout.0.size.height
contentHeight += buttonInset
}
contentHeight += bottomInset
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: UIEdgeInsets())
return (layout, { [weak self] in
guard let self else {
return
}
self.item = item
self.contentSize = layout.contentSize
self.insets = layout.insets
let _ = titleLayout.1()
let _ = textLayout.1()
let _ = searchAllMessagesTitleLayout.1()
var contentY: CGFloat = 0.0
contentY += topInset
let _ = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: "ChatListNoResults"
),
color: nil,
placeholderColor: nil,
startingPosition: .begin,
size: iconSize,
renderingScale: nil,
loop: false,
playOnce: nil
)),
environment: {}, containerSize: iconSize
)
let iconFrame = CGRect(origin: CGPoint(x: floor((params.width - iconSize.width) * 0.5), y: contentY), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.contentNode.view.addSubview(iconView)
}
iconView.frame = iconFrame
}
contentY += iconSize.height
contentY += iconSpacing
let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: contentY), size: titleLayout.0.size)
self.titleNode.frame = titleFrame
contentY += titleLayout.0.size.height
contentY += titleSpacing
let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: contentY), size: textLayout.0.size)
self.textNode.frame = textFrame
contentY += textLayout.0.size.height
if item.searchAllMessages != nil {
contentY += buttonSpacing
let searchAllMessagesButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - searchAllMessagesTitleLayout.0.size.width) * 0.5), y: contentY), size: CGSize(width: searchAllMessagesTitleLayout.0.size.width, height: searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0))
contentY += searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0
self.searchAllMessagesButton.frame = searchAllMessagesButtonFrame
self.searchAllMessagesTitle.frame = CGRect(origin: CGPoint(x: 0.0, y: buttonInset), size: searchAllMessagesTitleLayout.0.size)
contentY += buttonInset
contentY += searchAllMessagesTitleLayout.0.size.height
contentY += buttonInset
}
contentY += bottomInset
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: self.contentNode.frame.minY), size: CGSize(width: params.width, height: contentHeight))
self.contentNode.frame = contentFrame
})
}
}
}

View File

@ -419,7 +419,12 @@ final class VideoChatParticipantVideoComponent: Component {
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
} }
let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription) let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
if component.isMyPeer && component.isPresentation {
videoDescription = nil
} else {
videoDescription = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription)
}
var isEffectivelyPaused = false var isEffectivelyPaused = false
if let videoDescription, videoDescription.isPaused { if let videoDescription, videoDescription.isPaused {

View File

@ -152,16 +152,15 @@ extension VideoChatScreenComponent.View {
} }
} }
//TODO:localize
let qualityList: [(Int, String)] = [ let qualityList: [(Int, String)] = [
(0, "Audio Only"), (0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly),
(180, "180p"), (180, "180p"),
(360, "360p"), (360, "360p"),
(Int.max, "720p") (Int.max, "720p")
] ]
let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? "" let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? ""
items.append(.action(ContextMenuActionItem(text: "Receive Video Quality", textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in
return nil return nil
}, action: { [weak self] c, _ in }, action: { [weak self] c, _ in
guard let self else { guard let self else {