Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,47 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AttachmentFileController",
module_name = "AttachmentFileController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUIPreferences",
"//submodules/LegacyComponents",
"//submodules/SolidRoundedButtonNode",
"//submodules/PresentationDataUtils",
"//submodules/UIKitRuntimeUtils",
"//submodules/ComponentFlow",
"//submodules/ItemListPeerActionItem",
"//submodules/ListMessageItem",
"//submodules/AttachmentUI",
"//submodules/SearchBarNode",
"//submodules/MergeLists",
"//submodules/ChatListSearchItemHeader",
"//submodules/ItemListUI",
"//submodules/SearchUI",
"//submodules/ContextUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,675 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
import AttachmentUI
import TelegramStringFormatting
import ListMessageItem
import ComponentFlow
import GlassBarButtonComponent
import BundleIconComponent
import EdgeEffect
import SaveToCameraRoll
private final class AttachmentFileControllerArguments {
let context: AccountContext
let isAudio: Bool
let openGallery: () -> Void
let openFiles: () -> Void
let expandSavedMusic: () -> Void
let send: (Message) -> Void
init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) {
self.context = context
self.isAudio = isAudio
self.openGallery = openGallery
self.openFiles = openFiles
self.expandSavedMusic = expandSavedMusic
self.send = send
}
}
private enum AttachmentFileSection: Int32 {
case select
case savedMusic
case recent
}
private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool {
guard let lhsMessage = lhsMessage, let rhsMessage = rhsMessage else {
return lhsMessage == nil && rhsMessage == nil
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
private enum AttachmentFileEntry: ItemListNodeEntry {
case selectFromGallery(PresentationTheme, String)
case selectFromFiles(PresentationTheme, String)
case savedHeader(PresentationTheme, String)
case savedFile(Int32, PresentationTheme, Message?)
case showMore(PresentationTheme, String)
case recentHeader(PresentationTheme, String)
case file(Int32, PresentationTheme, Message?)
var section: ItemListSectionId {
switch self {
case .selectFromGallery, .selectFromFiles:
return AttachmentFileSection.select.rawValue
case .savedHeader, .savedFile, .showMore:
return AttachmentFileSection.savedMusic.rawValue
case .recentHeader, .file:
return AttachmentFileSection.recent.rawValue
}
}
var stableId: Int32 {
switch self {
case .selectFromGallery:
return 0
case .selectFromFiles:
return 1
case .savedHeader:
return 2
case let .savedFile(index, _, _):
return 3 + index
case .showMore:
return 9999
case .recentHeader:
return 10000
case let .file(index, _, _):
return 10001 + index
}
}
static func ==(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool {
switch lhs {
case let .selectFromGallery(lhsTheme, lhsText):
if case let .selectFromGallery(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .selectFromFiles(lhsTheme, lhsText):
if case let .selectFromFiles(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .savedHeader(lhsTheme, lhsText):
if case let .savedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .savedFile(lhsIndex, lhsTheme, lhsMessage):
if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
return true
} else {
return false
}
case let .showMore(lhsTheme, lhsText):
if case let .showMore(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .recentHeader(lhsTheme, lhsText):
if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .file(lhsIndex, lhsTheme, lhsMessage):
if case let .file(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
return true
} else {
return false
}
}
}
static func <(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! AttachmentFileControllerArguments
switch self {
case let .selectFromGallery(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.imageIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openGallery()
})
case let .selectFromFiles(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openFiles()
})
case let .savedHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .savedFile(_, _, message):
let interaction = ListMessageItemInteraction(openMessage: { message, _ in
arguments.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
case let .showMore(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandSavedMusic()
})
case let .recentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .file(_, _, message):
let interaction = ListMessageItemInteraction(openMessage: { message, _ in
arguments.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
}
}
}
private func attachmentFileControllerEntries(presentationData: PresentationData, mode: AttachmentFileControllerMode, state: AttachmentFileControllerState, savedMusic: [Message]?, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] {
guard !empty else {
return []
}
var entries: [AttachmentFileEntry] = []
if case .recent = mode {
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
}
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
let listTitle: String
switch mode {
case .recent:
listTitle = presentationData.strings.Attachment_RecentlySentFiles
case .audio:
listTitle = presentationData.strings.Attachment_SharedAudio
}
if case .audio = mode {
if let savedMusic, savedMusic.count > 0 {
entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased()))
var savedMusic = savedMusic
var showMore = false
if savedMusic.count > 4 && !state.savedMusicExpanded {
savedMusic = Array(savedMusic.prefix(3))
showMore = true
}
var i: Int32 = 0
for file in savedMusic {
entries.append(.savedFile(i, presentationData.theme, file))
i += 1
}
if showMore {
entries.append(.showMore(presentationData.theme, "Show More"))
}
}
}
if let recentDocuments = recentDocuments {
if recentDocuments.count > 0 {
entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
var i: Int32 = 0
for file in recentDocuments {
entries.append(.file(i, presentationData.theme, file))
i += 1
}
}
} else {
entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
for i in 0 ..< 11 {
entries.append(.file(Int32(i), presentationData.theme, nil))
}
}
return entries
}
private final class AttachmentFileContext: AttachmentMediaPickerContext {
}
public class AttachmentFileControllerImpl: ItemListController, AttachmentFileController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var parentController: () -> ViewController? = {
return nil
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
public var isMinimized: Bool = false
var delayDisappear = false
var hasBottomEdgeEffect = true
var resetForReuseImpl: () -> Void = {}
public func resetForReuse() {
self.resetForReuseImpl()
self.scrollToTop?()
}
public func prepareForReuse() {
self.delayDisappear = true
self.visibleBottomContentOffsetChanged?(self.visibleBottomContentOffset)
self.delayDisappear = false
}
public var mediaPickerContext: AttachmentMediaPickerContext? {
return AttachmentFileContext()
}
private var topEdgeEffectView: EdgeEffectView?
private var bottomEdgeEffectView: EdgeEffectView?
var isSearching: Bool = false {
didSet {
self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let topEdgeEffectView: EdgeEffectView
if let current = self.topEdgeEffectView {
topEdgeEffectView = current
} else {
topEdgeEffectView = EdgeEffectView()
if let navigationBar = self.navigationBar {
self.view.insertSubview(topEdgeEffectView, belowSubview: navigationBar.view)
}
self.topEdgeEffectView = topEdgeEffectView
}
let edgeEffectHeight: CGFloat = 88.0
let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame)
topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
if self.hasBottomEdgeEffect {
let bottomEdgeEffectView: EdgeEffectView
if let current = self.bottomEdgeEffectView {
bottomEdgeEffectView = current
} else {
bottomEdgeEffectView = EdgeEffectView()
self.view.addSubview(bottomEdgeEffectView)
self.bottomEdgeEffectView = bottomEdgeEffectView
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0)
bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
} else if let bottomEdgeEffectView = self.bottomEdgeEffectView {
bottomEdgeEffectView.removeFromSuperview()
}
}
}
private struct AttachmentFileControllerState: Equatable {
var searching: Bool
var savedMusicExpanded: Bool
}
public enum AttachmentFileControllerMode {
case recent
case audio
}
public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: AttachmentFileControllerMode = .recent, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController {
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false), ignoreRepeated: true)
let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false))
let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var updateTabBarVisibilityImpl: ((Bool) -> Void)?
var expandImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var updateIsSearchingImpl: ((Bool) -> Void)?
let arguments = AttachmentFileControllerArguments(
context: context,
isAudio: mode == .audio,
openGallery: {
presentGallery()
},
openFiles: {
presentFiles()
},
expandSavedMusic: {
updateState { state in
var updatedState = state
updatedState.savedMusicExpanded = true
return updatedState
}
},
send: { message in
if message.id.namespace == Namespaces.Message.Local {
if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.standalone(media: file))
}
} else {
let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
|> `catch` { _ in
return .single(.result([]))
}
|> mapToSignal { result -> Signal<[Message], NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> deliverOnMainQueue).startStandalone(next: { messages in
if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.message(message: MessageReference(message), media: file))
}
dismissImpl?()
})
}
}
)
let recentDocuments: Signal<[Message]?, NoError>
let savedMusicContext: ProfileSavedMusicContext?
let savedMusic: Signal<[Message]?, NoError>
switch mode {
case .recent:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
savedMusicContext = nil
savedMusic = .single(nil)
case .audio:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: context.account.peerId)
savedMusic = .single(nil)
|> then(
savedMusicContext!.state
|> map { state in
let peerId = context.account.peerId
var messages: [Message] = []
let peers = SimpleDictionary<PeerId, Peer>()
// if let peer {
// peers[peerId] = peer._asPeer()
// }
for file in state.files {
let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
}
return messages
}
)
}
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let existingCloseButton = Atomic<BarComponentHostNode?>(value: nil)
let existingSearchButton = Atomic<BarComponentHostNode?>(value: nil)
let previousRecentDocuments = Atomic<[Message]?>(value: nil)
let signal = combineLatest(queue: Queue.mainQueue(),
presentationData,
recentDocuments,
savedMusic,
statePromise.get()
)
|> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let barButtonSize = CGSize(width: 40.0, height: 40.0)
let closeButton = GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: presentationData.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
dismissImpl?()
}
)
let closeButtonComponent = AnyComponentWithIdentity(id: "close", component: AnyComponent(closeButton))
let closeButtonNode = existingCloseButton.modify { current in
let buttonNode: BarComponentHostNode
if let current {
buttonNode = current
buttonNode.component = closeButtonComponent
} else {
buttonNode = BarComponentHostNode(component: closeButtonComponent, size: barButtonSize)
}
return buttonNode
}
let searchButton = GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: presentationData.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Search",
tintColor: presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
updateState { state in
var updatedState = state
updatedState.searching = true
return updatedState
}
updateTabBarVisibilityImpl?(false)
updateIsSearchingImpl?(true)
}
)
let searchButtonComponent = state.searching ? nil : AnyComponentWithIdentity(id: "search", component: AnyComponent(searchButton))
let searchButtonNode = existingSearchButton.modify { current in
let buttonNode: BarComponentHostNode
if let current {
buttonNode = current
buttonNode.component = searchButtonComponent
} else {
buttonNode = BarComponentHostNode(component: searchButtonComponent, size: barButtonSize)
}
return buttonNode
}
let previousRecentDocuments = previousRecentDocuments.swap(recentDocuments)
let crossfade = previousRecentDocuments == nil && recentDocuments != nil
var animateChanges = false
if let previousRecentDocuments = previousRecentDocuments,
let recentDocuments = recentDocuments,
!previousRecentDocuments.isEmpty && !recentDocuments.isEmpty,
!crossfade {
animateChanges = true
}
let leftNavigationButton = closeButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) }
var rightNavigationButton: ItemListNavigationButton?
if bannedSendMedia == nil && (recentDocuments == nil || (recentDocuments?.count ?? 0) > 10) {
rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) }
}
let title: String
switch mode {
case .recent:
title = presentationData.strings.Attachment_File
case .audio:
title = "Audio"
}
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(title),
leftNavigationButton: leftNavigationButton,
rightNavigationButton: rightNavigationButton,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: true
)
var emptyItem: AttachmentFileEmptyStateItem?
if let (untilDate, personal) = bannedSendMedia {
let banDescription: String
if untilDate != 0 && untilDate != Int32.max {
banDescription = presentationData.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)).string
} else if personal {
banDescription = presentationData.strings.Conversation_RestrictedMedia
} else {
banDescription = presentationData.strings.Conversation_DefaultRestrictedMedia
}
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .bannedSendMedia(text: banDescription, canBoost: false))
} else if let recentDocuments = recentDocuments,
recentDocuments.isEmpty {
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .intro)
}
var searchItem: ItemListControllerSearch?
if state.searching {
searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: {
expandImpl?()
}, cancel: {
updateState { state in
var updatedState = state
updatedState.searching = false
return updatedState
}
updateTabBarVisibilityImpl?(true)
updateIsSearchingImpl?(false)
}, send: { message in
arguments.send(message)
}, dismissInput: {
dismissInputImpl?()
})
}
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
let _ = savedMusicContext?.state
}
let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true)
if case .audio = mode {
controller.hasBottomEdgeEffect = false
}
controller.delayDisappear = true
controller.visibleBottomContentOffsetChanged = { [weak controller] offset in
switch offset {
case let .known(value):
let backgroundAlpha: CGFloat = min(30.0, max(0.0, value)) / 30.0
if backgroundAlpha.isZero && controller?.delayDisappear == true {
Queue.mainQueue().after(0.25, {
controller?.updateTabBarAlpha(backgroundAlpha, .animated(duration: 0.1, curve: .easeInOut))
})
} else {
controller?.updateTabBarAlpha(backgroundAlpha, .immediate)
}
case .unknown, .none:
controller?.updateTabBarAlpha(1.0, .immediate)
controller?.delayDisappear = false
}
}
controller.resetForReuseImpl = {
updateState { state in
var updatedState = state
updatedState.searching = false
return updatedState
}
}
updateIsSearchingImpl = { [weak controller] isSearching in
controller?.isSearching = isSearching
}
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
expandImpl = { [weak controller] in
controller?.requestAttachmentMenuExpansion()
}
updateTabBarVisibilityImpl = { [weak controller] isVisible in
controller?.updateTabBarVisibility(isVisible, .animated(duration: 0.4, curve: .spring))
}
return controller
}
public func storyAudioPickerController(
context: AccountContext,
selectFromFiles: @escaping () -> Void,
dismissed: @escaping () -> Void,
completion: @escaping (AnyMediaReference) -> Void,
) -> ViewController {
var dismissImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
controller.requestController = { _, present in
let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: {
selectFromFiles()
dismissImpl?()
}, send: { file in
completion(file)
dismissImpl?()
}) as! AttachmentFileControllerImpl
present(filePickerController, filePickerController.mediaPickerContext)
}
controller.navigationPresentation = .flatModal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.didDismiss = {
dismissed()
}
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
return controller
}
@@ -0,0 +1,151 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
import SolidRoundedButtonNode
final class AttachmentFileEmptyStateItem: ItemListControllerEmptyStateItem {
enum Content: Equatable {
case intro
case bannedSendMedia(text: String, canBoost: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let content: Content
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content) {
self.context = context
self.theme = theme
self.strings = strings
self.content = content
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let item = to as? AttachmentFileEmptyStateItem {
return self.theme === item.theme && self.strings === item.strings && self.content == item.content
} else {
return false
}
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? AttachmentFileEmptyStateItemNode {
current.item = self
return current
} else {
return AttachmentFileEmptyStateItemNode(item: self)
}
}
}
final class AttachmentFileEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private var animationNode: AnimatedStickerNode
private let textNode: ASTextNode
private let buttonNode: SolidRoundedButtonNode
private var validLayout: (ContainerViewLayout, CGFloat)?
var item: AttachmentFileEmptyStateItem {
didSet {
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if let (layout, navigationHeight) = self.validLayout {
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
}
init(item: AttachmentFileEmptyStateItem) {
self.item = item
let name: String
let playbackMode: AnimatedStickerPlaybackMode
switch item.content {
case .intro:
name = "Files"
playbackMode = .loop
case .bannedSendMedia:
name = "Banned"
playbackMode = .once
}
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: name), width: 320, height: 320, playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 52.0, cornerRadius: 26.0, isShimmering: true)
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if case .bannedSendMedia(_, true) = item.content {
self.addSubnode(self.buttonNode)
}
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
let text: String
switch self.item.content {
case .intro:
text = strings.Attachment_FilesIntro
case let .bannedSendMedia(banDescription, _):
text = banDescription
}
self.textNode.attributedText = NSAttributedString(string: text.replacingOccurrences(of: "\n", with: " "), font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.buttonNode.title = strings.Attachment_OpenSettings
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var imageSize = CGSize(width: 144.0, height: 144.0)
var insets = layout.insets(options: [])
if layout.size.width == 320.0 {
insets.top += -60.0
imageSize = CGSize(width: 112.0, height: 112.0)
} else {
insets.top += 30.0 //-160.0
}
let imageSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 12.0
let buttonSpacing: CGFloat = 15.0
let bottomSpacing: CGFloat = 33.0
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
let buttonWidth: CGFloat = 248.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + textSpacing + textSize.height + buttonSpacing + buttonHeight + bottomSpacing
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
self.animationNode.updateLayout(size: imageSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight + textSpacing), size: textSize))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - buttonWidth - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.textNode.frame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight)))
}
}
@@ -0,0 +1,573 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import ItemListUI
import PresentationDataUtils
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import MergeLists
import ChatListSearchItemHeader
import ItemListUI
import SearchUI
import ContextUI
import ListMessageItem
import ComponentFlow
import SearchInputPanelComponent
final class AttachmentFileSearchItem: ItemListControllerSearch {
let context: AccountContext
let mode: AttachmentFileControllerMode
let presentationData: PresentationData
let focus: () -> Void
let cancel: () -> Void
let send: (Message) -> Void
let dismissInput: () -> Void
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
self.send = send
self.dismissInput = dismissInput
self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
if value {
return .single(value) |> delay(0.2, queue: Queue.mainQueue())
} else {
return .single(value)
}
}).startStrict(next: { [weak self] value in
self?.updateActivity?(value)
}))
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? AttachmentFileSearchItem {
if self.context !== to.context {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)? {
return nil
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
return AttachmentFileSearchItemNode(context: self.context, mode: self.mode, presentationData: self.presentationData, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
self?.activity.set(value)
}, dismissInput: self.dismissInput)
}
}
private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
private let mode: AttachmentFileControllerMode
private let presentationData: PresentationData
private let focus: () -> Void
private let cancel: () -> Void
private let containerNode: AttachmentFileSearchContainerNode
private let searchInput = ComponentView<Empty>()
private var validLayout: ContainerViewLayout?
init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
self.containerNode = AttachmentFileSearchContainerNode(context: context, mode: mode, presentationData: presentationData, send: { message in
send(message)
}, updateActivity: updateActivity)
super.init()
self.addedUnderNavigationBar = true
self.addSubnode(self.containerNode)
self.containerNode.cancel = { [weak self] in
dismissInput()
cancel()
self?.deactivateInput()
}
self.containerNode.dismissInput = {
dismissInput()
}
}
override func queryUpdated(_ query: String) {
self.containerNode.searchTextUpdated(text: query)
}
override func scrollToTop() {
self.containerNode.scrollToTop()
}
private func deactivateInput() {
if let layout = self.validLayout, let searchInputView = self.searchInput.view as? SearchInputPanelComponent.View {
let transition = ComponentTransition.spring(duration: 0.4)
transition.setFrame(view: searchInputView, frame: CGRect(origin: CGPoint(x: searchInputView.frame.minX, y: layout.size.height), size: searchInputView.frame.size))
}
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)))
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition)
let searchInputSize = self.searchInput.update(
transition: .immediate,
component: AnyComponent(
SearchInputPanelComponent(
theme: self.presentationData.theme,
strings: self.presentationData.strings,
metrics: layout.metrics,
safeInsets: layout.safeInsets,
placeholder: self.mode == .audio ? self.presentationData.strings.Attachment_FilesSearchPlaceholder : self.presentationData.strings.Attachment_FilesSearchPlaceholder,
updated: { [weak self] query in
guard let self else {
return
}
self.queryUpdated(query)
},
cancel: { [weak self] in
guard let self else {
return
}
self.cancel()
self.deactivateInput()
}
)
),
environment: {},
containerSize: CGSize(width: layout.size.width, height: layout.size.height)
)
let bottomInset: CGFloat = layout.insets(options: .input).bottom
let searchInputFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset - searchInputSize.height), size: searchInputSize)
if let searchInputView = self.searchInput.view as? SearchInputPanelComponent.View {
if searchInputView.superview == nil {
self.view.addSubview(searchInputView)
searchInputView.frame = CGRect(origin: CGPoint(x: searchInputFrame.minX, y: layout.size.height), size: searchInputFrame.size)
self.focus()
searchInputView.activateInput()
}
transition.updateFrame(view: searchInputView, frame: searchInputFrame)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let searchInputView = self.searchInput.view as? SearchInputPanelComponent.View {
if let result = searchInputView.hitTest(self.view.convert(point, to: searchInputView), with: event) {
return result
}
}
if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
private final class AttachmentFileSearchContainerInteraction {
let context: AccountContext
let send: (Message) -> Void
init(context: AccountContext, send: @escaping (Message) -> Void) {
self.context = context
self.send = send
}
}
private enum AttachmentFileSearchEntryId: Hashable {
case placeholder(Int)
case message(MessageId)
}
private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool {
guard let lhsMessage = lhsMessage, let rhsMessage = rhsMessage else {
return lhsMessage == nil && rhsMessage == nil
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
private final class AttachmentFileSearchEntry: Comparable, Identifiable {
let index: Int
let message: Message?
init(index: Int, message: Message?) {
self.index = index
self.message = message
}
var stableId: AttachmentFileSearchEntryId {
if let message = self.message {
return .message(message.id)
} else {
return .placeholder(self.index)
}
}
static func ==(lhs: AttachmentFileSearchEntry, rhs: AttachmentFileSearchEntry) -> Bool {
return lhs.index == rhs.index && areMessagesEqual(lhs.message, rhs.message)
}
static func <(lhs: AttachmentFileSearchEntry, rhs: AttachmentFileSearchEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction) -> ListViewItem {
let itemInteraction = ListMessageItemInteraction(openMessage: { message, _ in
interaction.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
}
}
struct AttachmentFileSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
let isEmpty: Bool
let query: String
}
private func attachmentFileSearchContainerPreparedRecentTransition(from fromEntries: [AttachmentFileSearchEntry], to toEntries: [AttachmentFileSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction) -> AttachmentFileSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
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, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
return AttachmentFileSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query)
}
public final class AttachmentFileSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let send: (Message) -> Void
private let dimNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let listNode: ListView
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private var enqueuedTransitions: [(AttachmentFileSearchContainerTransition, Bool)] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private let searchQuery = Promise<String?>()
private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
private var _hasDim: Bool = false
override public var hasDim: Bool {
return _hasDim
}
public init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
self.context = context
self.send = send
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = .clear
self.backgroundNode = ASDisplayNode()
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.emptyResultsTitleNode = ImmediateTextNode()
self.emptyResultsTitleNode.displaysAsynchronously = false
self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor)
self.emptyResultsTitleNode.textAlignment = .center
self.emptyResultsTitleNode.isHidden = true
self.emptyResultsTextNode = ImmediateTextNode()
self.emptyResultsTextNode.displaysAsynchronously = false
self.emptyResultsTextNode.maximumNumberOfLines = 0
self.emptyResultsTextNode.textAlignment = .center
self.emptyResultsTextNode.isHidden = true
super.init()
self.backgroundNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.backgroundNode.alpha = 0.0
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.alpha = 0.0
self._hasDim = true
self.addSubnode(self.dimNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.listNode)
self.addSubnode(self.emptyResultsTitleNode)
self.addSubnode(self.emptyResultsTextNode)
let interaction = AttachmentFileSearchContainerInteraction(context: context, send: { [weak self] message in
send(message)
self?.listNode.clearHighlightAnimated(true)
})
let presentationDataPromise = self.presentationDataPromise
let searchQuery = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> in
if let query = query, !query.isEmpty {
return (.complete() |> delay(0.6, queue: Queue.mainQueue()))
|> then(.single(query))
} else {
return .single(query)
}
}
let foundItems = searchQuery
|> mapToSignal { query -> Signal<[AttachmentFileSearchEntry]?, NoError> in
guard let query = query, !query.isEmpty else {
return .single(nil)
}
let signal: Signal<[Message]?, NoError>
switch mode {
case .recent:
signal = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
case .audio:
signal = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
}
updateActivity(true)
return combineLatest(signal, presentationDataPromise.get())
|> mapToSignal { messages, presentationData -> Signal<[AttachmentFileSearchEntry]?, NoError> in
var entries: [AttachmentFileSearchEntry] = []
var index = 0
if let messages = messages {
for message in messages {
entries.append(AttachmentFileSearchEntry(index: index, message: message))
index += 1
}
} else {
for _ in 0 ..< 16 {
entries.append(AttachmentFileSearchEntry(index: index, message: nil))
index += 1
}
}
return .single(entries)
}
}
let previousSearchItems = Atomic<[AttachmentFileSearchEntry]?>(value: nil)
self.searchDisposable.set((combineLatest(searchQuery, foundItems, self.presentationDataPromise.get())
|> deliverOnMainQueue).startStrict(next: { [weak self] query, entries, presentationData in
if let strongSelf = self {
let previousEntries = previousSearchItems.swap(entries)
updateActivity(false)
let firstTime = previousEntries == nil
let transition = attachmentFileSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
// self.presentationDataDisposable = (context.sharedContext.presentationData
// |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
// if let strongSelf = self {
// let previousTheme = strongSelf.presentationData.theme
// let previousStrings = strongSelf.presentationData.strings
//
// strongSelf.presentationData = presentationData
//
// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
// strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
// }
// }
// })
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
override public func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.backgroundNode.backgroundColor = theme.chatList.backgroundColor
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override public func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: AttachmentFileSearchContainerTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
let containerTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
containerTransition.updateAlpha(node: strongSelf.backgroundNode, alpha: isSearching ? 1.0 : 0.0)
containerTransition.updateAlpha(node: strongSelf.listNode, alpha: isSearching ? 1.0 : 0.0)
strongSelf.dimNode.isHidden = transition.isSearching
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
let emptyResults = transition.isSearching && transition.isEmpty
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
})
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let hadValidLayout = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -66.0), size: CGSize(width: layout.size.width, height: 66.0))
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let padding: CGFloat = 16.0
let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSpacing: CGFloat = 8.0
let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing
let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0)
transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize))
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
override public func scrollToTop() {
if self.listNode.alpha > 0.0 {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = self.view.hitTest(point, with: event) else {
return nil
}
if result === self.view {
return nil
}
return result
}
}