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,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "WallpaperGridScreen",
module_name = "WallpaperGridScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/WallpaperBackgroundNode",
"//submodules/ComponentFlow",
"//submodules/SolidRoundedButtonNode",
"//submodules/AppBundle",
"//submodules/PremiumUI",
"//submodules/WallpaperResources",
"//submodules/HexColor",
"//submodules/MergeLists",
"//submodules/ShareController",
"//submodules/GalleryUI",
"//submodules/GridMessageSelectionNode",
"//submodules/SearchUI",
"//submodules/MediaPickerUI",
"//submodules/ItemListPeerActionItem",
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
"//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen",
"//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent",
"//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,395 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AttachmentUI
import WallpaperGalleryScreen
import ThemeAccentColorScreen
private func availableGradients(dark: Bool) -> [[UInt32]] {
if dark {
return [
[0x1e3557, 0x151a36, 0x1c4352, 0x2a4541] as [UInt32],
[0x1d223f, 0x1d1832, 0x1b2943, 0x141631] as [UInt32],
[0x203439, 0x102028, 0x1d3c3a, 0x172635] as [UInt32],
[0x1c2731, 0x1a1c25, 0x27303b, 0x1b1b21] as [UInt32],
[0x3a1c3a, 0x24193c, 0x392e3e, 0x1a1632] as [UInt32],
[0x2c211b, 0x44332a, 0x22191f, 0x3b2d36] as [UInt32],
[0x1e3557, 0x182036, 0x1c4352, 0x16263a] as [UInt32],
[0x111236, 0x14424f, 0x0b2334, 0x3b315d] as [UInt32],
[0x2d4836, 0x172b19, 0x364331, 0x103231] as [UInt32]
]
} else {
return [
[0xdbddbb, 0x6ba587, 0xd5d88d, 0x88b884] as [UInt32],
[0x8dc0eb, 0xb9d1ea, 0xc6b1ef, 0xebd7ef] as [UInt32],
[0x97beeb, 0xb1e9ea, 0xc6b1ef, 0xefb7dc] as [UInt32],
[0x8adbf2, 0x888dec, 0xe39fea, 0x679ced] as [UInt32],
[0xb0cdeb, 0x9fb0ea, 0xbbead5, 0xb2e3dd] as [UInt32],
[0xdaeac8, 0xa2b4ff, 0xeccbff, 0xb9e2ff] as [UInt32],
[0xdceb92, 0x8fe1d6, 0x67a3f2, 0x85d685] as [UInt32],
[0xeaa36e, 0xf0e486, 0xf29ebf, 0xe8c06e] as [UInt32],
[0xffc3b2, 0xe2c0ff, 0xffe7b2, 0xf8cece] as [UInt32]
]
}
}
private func availableColors(dark: Bool) -> [UInt32] {
if dark {
return [
0x1D2D3C,
0x111B26,
0x0B141E,
0x1F361F,
0x131F15,
0x0E1710,
0x2F2E27,
0x2A261F,
0x191817,
0x432E30,
0x2E1C1E,
0x1F1314,
0x432E3C,
0x2E1C28,
0x1F131B,
0x3C2E43,
0x291C2E,
0x1D1221,
0x312E43,
0x1E1C2E,
0x141221,
0x2F3F3F,
0x212D30,
0x141E20,
0x272524,
0x191716,
0x000000
]
} else {
return [
0xD3DFEA,
0xA5C5DB,
0x6F99C8,
0xD2E3A9,
0xA4D48E,
0x7DBB6E,
0xE6DDAE,
0xD5BE91,
0xCBA479,
0xEBC0B9,
0xE0A79D,
0xC97870,
0xEBB9C8,
0xE09DB7,
0xD27593,
0xDAC2ED,
0xD3A5E7,
0xB587D2,
0xC2C2ED,
0xA5A5E7,
0x7F7FD0,
0xC2E2ED,
0xA5D6E7,
0x7FBAD0,
0xD6C2B9,
0x9C8882,
0x000000
]
}
}
public final class ThemeColorsGridController: ViewController, AttachmentContainable {
public enum Mode {
case `default`
case peer(EnginePeer)
var galleryMode: WallpaperGalleryController.Mode {
switch self {
case .default:
return .default
case let .peer(peer):
return .peer(peer, false)
}
}
var colorPickerMode: ThemeAccentColorController.ResultMode {
switch self {
case .default:
return .default
case let .peer(peer):
return .peer(peer)
}
}
}
private var controllerNode: ThemeColorsGridControllerNode {
return self.displayNode as! ThemeColorsGridControllerNode
}
private let _ready = Promise<Bool>()
public override var ready: Promise<Bool> {
return self._ready
}
private let context: AccountContext
let mode: Mode
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var validLayout: ContainerViewLayout?
private var previousContentOffset: GridNodeVisibleContentOffset?
fileprivate var mainButtonState: AttachmentMainButtonState?
var pushController: (ViewController) -> Void = { _ in }
var dismissControllers: (() -> Void)?
var openGallery: (() -> Void)?
public init(context: AccountContext, mode: Mode = .default, canDelete: Bool = false) {
self.context = context
self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.controllerNode.scrollToTop()
}
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(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()
}
}
})
if case .peer = mode {
self.title = self.presentationData.strings.Conversation_Theme_ChooseColorTitle
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Conversation_Theme_SetCustomColor, style: .plain, target: self, action: #selector(self.customPressed))
} else {
self.title = self.presentationData.strings.WallpaperColors_Title
}
self.pushController = { [weak self] controller in
self?.push(controller)
}
self.mainButtonState = AttachmentMainButtonState(text: self.presentationData.strings.Conversation_Theme_SetPhotoWallpaper, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
@objc private func cancelPressed() {
self.dismiss()
}
@objc private func customPressed() {
self.presentColorPicker()
}
private func updateThemeAndStrings() {
self.title = self.presentationData.strings.WallpaperColors_Title
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
if self.isNodeLoaded {
self.controllerNode.updatePresentationData(self.presentationData)
}
}
private func presentColorPicker() {
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings])
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
guard let strongSelf = self else {
return
}
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered
let themeReference: PresentationThemeReference
if autoNightModeTriggered {
themeReference = settings.automaticThemeSwitchSetting.theme
} else {
themeReference = settings.theme
}
let controller = ThemeAccentColorController(context: strongSelf.context, mode: .background(themeReference: themeReference), resultMode: strongSelf.mode.colorPickerMode)
controller.completion = { [weak self, weak controller] in
if let strongSelf = self {
if let dismissControllers = strongSelf.dismissControllers {
dismissControllers()
controller?.dismiss(animated: true)
} else if let navigationController = strongSelf.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
controllers = controllers.filter { controller in
if controller is ThemeColorsGridController {
return false
}
return true
}
navigationController.setViewControllers(controllers, animated: false)
controllers = controllers.filter { controller in
if controller is ThemeAccentColorController {
return false
}
return true
}
navigationController.setViewControllers(controllers, animated: true)
}
}
}
strongSelf.pushController(controller)
})
}
public override func loadDisplayNode() {
var dark = false
if case .default = self.mode {
dark = self.presentationData.theme.overallDarkAppearance
}
self.displayNode = ThemeColorsGridControllerNode(context: self.context, presentationData: self.presentationData, controller: self, gradients: availableGradients(dark: dark), colors: availableColors(dark: dark), push: { [weak self] controller in
self?.pushController(controller)
}, pop: { [weak self] in
if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController {
let _ = navigationController.popViewController(animated: true)
}
}, presentColorPicker: { [weak self] in
if let strongSelf = self {
strongSelf.presentColorPicker()
}
})
let transitionOffset: CGFloat
switch self.mode {
case .default:
transitionOffset = 30.0
case .peer:
transitionOffset = 2.0
}
self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > transitionOffset {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.navigationBar?.updateBackgroundAlpha(min(transitionOffset, value) / transitionOffset, transition: transition)
case .unknown, .none:
strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
strongSelf.previousContentOffset = offset
}
}
self._ready.set(self.controllerNode.ready.get())
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.displayNodeDidLoad()
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc fileprivate func mainButtonPressed() {
self.dismiss(animated: true)
self.openGallery?()
}
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
public var mediaPickerContext: AttachmentMediaPickerContext? {
return ThemeColorsGridContext(controller: self)
}
}
private final class ThemeColorsGridContext: AttachmentMediaPickerContext {
private weak var controller: ThemeColorsGridController?
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
return .single(self.controller?.mainButtonState)
}
init(controller: ThemeColorsGridController) {
self.controller = controller
}
func mainButtonAction() {
self.controller?.mainButtonPressed()
}
}
public func standaloneColorPickerController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
peer: EnginePeer,
push: @escaping (ViewController) -> Void,
openGallery: @escaping () -> Void
) -> ViewController {
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil
})
controller.requestController = { _, present in
let colorPickerController = ThemeColorsGridController(context: context, mode: .peer(peer))
colorPickerController.pushController = { controller in
push(controller)
}
colorPickerController.dismissControllers = { [weak controller] in
controller?.dismiss(animated: true)
}
colorPickerController.openGallery = openGallery
present(colorPickerController, colorPickerController.mediaPickerContext)
}
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
return controller
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import AccountContext
import GridMessageSelectionNode
import SettingsThemeWallpaperNode
final class ThemeColorsGridControllerItem: GridItem {
let context: AccountContext
let wallpaper: TelegramWallpaper
let selected: Bool
let interaction: ThemeColorsGridControllerInteraction
let section: GridSection? = nil
init(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, interaction: ThemeColorsGridControllerInteraction) {
self.context = context
self.wallpaper = wallpaper
self.selected = selected
self.interaction = interaction
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ThemeColorsGridControllerItemNode()
node.setup(context: self.context, wallpaper: self.wallpaper, selected: self.selected, interaction: self.interaction)
return node
}
func update(node: GridItemNode) {
guard let node = node as? ThemeColorsGridControllerItemNode else {
assertionFailure()
return
}
node.setup(context: self.context, wallpaper: self.wallpaper, selected: self.selected, interaction: self.interaction)
}
}
final class ThemeColorsGridControllerItemNode: GridItemNode {
private let wallpaperNode: SettingsThemeWallpaperNode
private var selectionNode: GridMessageSelectionNode?
private var currentState: (AccountContext, TelegramWallpaper, Bool)?
private var interaction: ThemeColorsGridControllerInteraction?
override init() {
self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false)
super.init()
self.addSubnode(self.wallpaperNode)
}
override func didLoad() {
super.didLoad()
self.view.isExclusiveTouch = true
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func setup(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, interaction: ThemeColorsGridControllerInteraction) {
self.interaction = interaction
if self.currentState == nil || self.currentState!.0 !== context || wallpaper != self.currentState!.1 || selected != self.currentState!.2 {
self.currentState = (context, wallpaper, selected)
self.setNeedsLayout()
}
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let (_, wallpaper, _) = self.currentState {
self.interaction?.openWallpaper(wallpaper)
}
}
}
override func layout() {
super.layout()
let bounds = self.bounds
if let (context, wallpaper, selected) = self.currentState {
self.wallpaperNode.setWallpaper(context: context, wallpaper: wallpaper, selected: selected, size: bounds.size)
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size)
}
}
}
@@ -0,0 +1,412 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperGalleryScreen
final class ThemeColorsGridControllerInteraction {
let openWallpaper: (TelegramWallpaper) -> Void
init(openWallpaper: @escaping (TelegramWallpaper) -> Void) {
self.openWallpaper = openWallpaper
}
}
private struct ThemeColorsGridControllerEntry: Comparable, Identifiable {
let index: Int
let wallpaper: TelegramWallpaper
let selected: Bool
static func ==(lhs: ThemeColorsGridControllerEntry, rhs: ThemeColorsGridControllerEntry) -> Bool {
return lhs.index == rhs.index && lhs.wallpaper == rhs.wallpaper && lhs.selected == rhs.selected
}
static func <(lhs: ThemeColorsGridControllerEntry, rhs: ThemeColorsGridControllerEntry) -> Bool {
return lhs.index < rhs.index
}
var stableId: Int {
return self.index
}
func item(context: AccountContext, interaction: ThemeColorsGridControllerInteraction) -> ThemeColorsGridControllerItem {
return ThemeColorsGridControllerItem(context: context, wallpaper: self.wallpaper, selected: self.selected, interaction: interaction)
}
}
private struct ThemeColorsGridEntryTransition {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let updateFirstIndexInSectionOffset: Int?
let stationaryItems: GridNodeStationaryItems
let scrollToItem: GridNodeScrollToItem?
}
private func preparedThemeColorsGridEntryTransition(context: AccountContext, from fromEntries: [ThemeColorsGridControllerEntry], to toEntries: [ThemeColorsGridControllerEntry], interaction: ThemeColorsGridControllerInteraction) -> ThemeColorsGridEntryTransition {
let stationaryItems: GridNodeStationaryItems = .none
let scrollToItem: GridNodeScrollToItem? = nil
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction), previousIndex: $0.2) }
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction)) }
return ThemeColorsGridEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem)
}
final class ThemeColorsGridControllerNode: ASDisplayNode {
private let context: AccountContext
private weak var controller: ThemeColorsGridController?
private var presentationData: PresentationData
private var controllerInteraction: ThemeColorsGridControllerInteraction?
private let push: (ViewController) -> Void
private let presentColorPicker: () -> Void
let ready = ValuePromise<Bool>()
private var topBackgroundNode: ASDisplayNode
private var separatorNode: ASDisplayNode
private let customColorItemNode: ItemListActionItemNode
private var customColorItem: ItemListActionItem
let gridNode: GridNode
private let leftOverlayNode: ASDisplayNode
private let rightOverlayNode: ASDisplayNode
private var queuedTransitions: [ThemeColorsGridEntryTransition] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private var disposable: Disposable?
init(context: AccountContext, presentationData: PresentationData, controller: ThemeColorsGridController, gradients: [[UInt32]], colors: [UInt32], push: @escaping (ViewController) -> Void, pop: @escaping () -> Void, presentColorPicker: @escaping () -> Void) {
self.context = context
self.controller = controller
self.presentationData = presentationData
self.push = push
self.presentColorPicker = presentColorPicker
self.gridNode = GridNode()
self.gridNode.showVerticalScrollIndicator = false
self.leftOverlayNode = ASDisplayNode()
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.topBackgroundNode = ASDisplayNode()
self.topBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.customColorItemNode = ItemListActionItemNode()
self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: {
presentColorPicker()
})
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
if case .default = controller.mode {
self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
self.gridNode.addSubnode(self.topBackgroundNode)
self.gridNode.addSubnode(self.separatorNode)
self.gridNode.addSubnode(self.customColorItemNode)
} else {
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
}
self.addSubnode(self.gridNode)
let previousEntries = Atomic<[ThemeColorsGridControllerEntry]?>(value: nil)
let interaction = ThemeColorsGridControllerInteraction(openWallpaper: { [weak self] wallpaper in
if let strongSelf = self {
let entries = previousEntries.with { $0 }
if let entries = entries, !entries.isEmpty {
let wallpapers = entries.map { $0.wallpaper }
let controller = WallpaperGalleryController(context: context, source: .list(wallpapers: wallpapers, central: wallpaper, type: .colors), mode: strongSelf.controller?.mode.galleryMode ?? .default)
let dismissControllers = { [weak self, weak controller] in
if let self {
if let dismissControllers = self.controller?.dismissControllers {
dismissControllers()
controller?.dismiss(animated: true)
} else if let navigationController = self.controller?.navigationController as? NavigationController {
let controllers = navigationController.viewControllers.filter({ controller in
if controller is ThemeColorsGridController || controller is WallpaperGalleryController {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: true)
}
}
}
controller.navigationPresentation = .modal
controller.apply = { [weak self] wallpaper, _, _, _, _, forBoth in
if let strongSelf = self, let mode = strongSelf.controller?.mode, case let .peer(peer) = mode, case let .wallpaper(wallpaperValue, _) = wallpaper {
let _ = (strongSelf.context.engine.themes.setChatWallpaper(peerId: peer.id, wallpaper: wallpaperValue, forBoth: forBoth)
|> deliverOnMainQueue).start(completed: {
dismissControllers()
})
} else {
pop()
}
}
strongSelf.push(controller)
}
}
})
self.controllerInteraction = interaction
var wallpapers: [TelegramWallpaper] = []
wallpapers.append(contentsOf: gradients.map { TelegramWallpaper.gradient(TelegramWallpaper.Gradient(id: nil, colors: $0, settings: WallpaperSettings())) })
wallpapers.append(contentsOf: colors.map { TelegramWallpaper.color($0) })
let transition = context.sharedContext.presentationData
|> map { presentationData -> (ThemeColorsGridEntryTransition, Bool) in
var entries: [ThemeColorsGridControllerEntry] = []
var index = 0
for wallpaper in wallpapers {
let selected = presentationData.chatWallpaper == wallpaper
entries.append(ThemeColorsGridControllerEntry(index: index, wallpaper: wallpaper, selected: selected))
index += 1
}
let previous = previousEntries.swap(entries)
return (preparedThemeColorsGridEntryTransition(context: context, from: previous ?? [], to: entries, interaction: interaction), previous == nil)
}
self.disposable = (transition |> deliverOnMainQueue).start(next: { [weak self] (transition, _) in
if let strongSelf = self {
strongSelf.enqueueTransition(transition)
}
})
}
deinit {
self.disposable?.dispose()
}
override func didLoad() {
super.didLoad()
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
tapRecognizer.delaysTouchesBegan = false
tapRecognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
tapRecognizer.highlight = { [weak self] point in
if let strongSelf = self {
var highlightedNode: ListViewItemNode?
if let point = point {
if strongSelf.customColorItemNode.frame.contains(point) {
highlightedNode = strongSelf.customColorItemNode
}
}
if let highlightedNode = highlightedNode {
highlightedNode.setHighlighted(true, at: CGPoint(), animated: false)
} else {
strongSelf.customColorItemNode.setHighlighted(false, at: CGPoint(), animated: true)
}
}
}
self.gridNode.view.addGestureRecognizer(tapRecognizer)
}
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if self.customColorItemNode.frame.contains(location) {
self.customColorItem.action()
}
default:
break
}
}
default:
break
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
if let controller = self.controller, case .default = controller.mode {
self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
} else {
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
}
self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.topBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in
self?.presentColorPicker()
})
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
private func enqueueTransition(_ transition: ThemeColorsGridEntryTransition) {
self.queuedTransitions.append(transition)
if self.validLayout != nil {
self.dequeueTransitions()
}
}
private func dequeueTransitions() {
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.ready.set(true)
}
})
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
guard let controller = self.controller else {
return
}
let hadValidLayout = self.validLayout != nil
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
let scrollIndicatorInsets = insets
let itemsPerRow: Int
if case .compact = layout.metrics.widthClass {
switch layout.orientation {
case .portrait:
itemsPerRow = 3
case .landscape:
itemsPerRow = 5
}
} else {
itemsPerRow = 3
}
let referenceImageSize: CGSize
let screenWidth = min(layout.size.width, layout.size.height)
if screenWidth >= 375.0 {
referenceImageSize = CGSize(width: 108.0, height: 108.0)
} else {
referenceImageSize = CGSize(width: 91.0, height: 91.0)
}
let width = layout.size.width - layout.safeInsets.left - layout.safeInsets.right
let imageSize: CGSize
let spacing: CGFloat
var fillWidth: Bool?
if case .peer = controller.mode {
spacing = 1.0
let itemWidth = floorToScreenPixels((width - spacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
imageSize = CGSize(width: itemWidth, height: itemWidth)
fillWidth = true
} else {
let minSpacing = 8.0
imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((width - CGFloat(itemsPerRow + 1) * minSpacing) / CGFloat(itemsPerRow)), height: referenceImageSize.height))
spacing = floor((width - CGFloat(itemsPerRow) * imageSize.width) / CGFloat(itemsPerRow + 1))
}
let buttonTopInset: CGFloat = 32.0
let buttonHeight: CGFloat = 44.0
let buttonBottomInset: CGFloat = 35.0
var buttonInset: CGFloat = buttonTopInset + buttonHeight + buttonBottomInset
var buttonOffset = buttonInset + 10.0
var listInsets = insets
if case .default = controller.mode {
if layout.size.width >= 375.0 {
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
listInsets.left += inset
listInsets.right += inset
if self.leftOverlayNode.supernode == nil {
self.gridNode.addSubnode(self.leftOverlayNode)
}
if self.rightOverlayNode.supernode == nil {
self.gridNode.addSubnode(self.rightOverlayNode)
}
} else {
if self.leftOverlayNode.supernode != nil {
self.leftOverlayNode.removeFromSupernode()
}
if self.rightOverlayNode.supernode != nil {
self.rightOverlayNode.removeFromSupernode()
}
}
} else {
self.customColorItemNode.isHidden = true
buttonOffset = 0.0
buttonInset = 0.0
}
let makeColorLayout = self.customColorItemNode.asyncLayout()
let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: listInsets.left, rightInset: listInsets.right, availableHeight: layout.size.height)
let (colorLayout, colorApply) = makeColorLayout(self.customColorItem, params, ItemListNeighbors(top: .none, bottom: .none))
colorApply(false)
transition.updateFrame(node: self.topBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0)))
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
transition.updateFrame(node: self.customColorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset), size: colorLayout.contentSize))
self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + UIScreenPixel)
self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + UIScreenPixel)
insets.top += spacing + buttonInset
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: fillWidth, lineSpacing: spacing, itemSpacing: fillWidth != nil ? spacing : nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.validLayout = (layout, navigationBarHeight)
if !hadValidLayout {
self.dequeueTransitions()
}
}
func scrollToTop() {
let offset = self.gridNode.scrollView.contentOffset.y + self.gridNode.scrollView.contentInset.top
let duration: Double = 0.25
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
self.topBackgroundNode.layer.animatePosition(from: self.topBackgroundNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.topBackgroundNode.layer.position, duration: duration)
self.separatorNode.layer.animatePosition(from: self.separatorNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.separatorNode.layer.position, duration: duration)
self.customColorItemNode.layer.animatePosition(from: self.customColorItemNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.customColorItemNode.layer.position, duration: duration)
}
}
@@ -0,0 +1,544 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import OverlayStatusController
import AccountContext
import ShareController
import SearchUI
import HexColor
import PresentationDataUtils
import MediaPickerUI
import WallpaperGalleryScreen
import Photos
public enum WallpaperSelectionResult {
case remove
case emoticon(String)
case custom(wallpaperEntry: WallpaperGalleryEntry, options: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?)
}
public final class ThemeGridController: ViewController {
public enum Mode {
case generic
case peer(EnginePeer, [TelegramTheme], TelegramWallpaper?, Int?, Int?)
}
private var controllerNode: ThemeGridControllerNode {
return self.displayNode as! ThemeGridControllerNode
}
private let _ready = Promise<Bool>()
public override var ready: Promise<Bool> {
return self._ready
}
private let context: AccountContext
private let mode: Mode
private var presentationData: PresentationData
private let presentationDataPromise = Promise<PresentationData>()
private var presentationDataDisposable: Disposable?
private var searchContentNode: NavigationBarSearchContentNode?
private var isEmpty: Bool?
private var editingMode: Bool = false
private var validLayout: ContainerViewLayout?
public override var navigationBarRequiresEntireLayoutUpdate: Bool {
return false
}
private var previousContentOffset: GridNodeVisibleContentOffset?
public var completion: (WallpaperSelectionResult) -> Void = { _ in }
public init(context: AccountContext, mode: Mode = .generic) {
self.context = context
self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise.set(.single(self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
switch mode {
case .generic:
self.title = self.presentationData.strings.Wallpaper_Title
case .peer:
self.title = self.presentationData.strings.Wallpaper_ChannelTitle
}
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.controllerNode.scrollToTop()
}
}
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
strongSelf.presentationDataPromise.set(.single(presentationData))
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
private func updateThemeAndStrings() {
self.title = self.presentationData.strings.Wallpaper_Title
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
if case .generic = self.mode {
if let isEmpty = self.isEmpty, isEmpty {
} else {
if self.editingMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
}
}
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Wallpaper_Search)
if self.isNodeLoaded {
self.controllerNode.updatePresentationData(self.presentationData)
}
}
public override func loadDisplayNode() {
var mode: WallpaperGalleryController.Mode = .default
var requiredLevel: Int?
var requiredCustomLevel: Int?
if case let .peer(peer, _, _, requiredLevelValue, requiredCustomLevelValue) = self.mode {
mode = .peer(peer, false)
requiredLevel = requiredLevelValue
requiredCustomLevel = requiredCustomLevelValue
}
self.displayNode = ThemeGridControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, presentPreviewController: { [weak self] source in
if let strongSelf = self {
let dismissControllers = { [weak self] in
if let self, let navigationController = self.navigationController as? NavigationController {
var controllers = navigationController.viewControllers.filter({ controller in
if controller is ThemeGridController {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: false)
controllers = navigationController.viewControllers.filter({ controller in
if controller is WallpaperGalleryController {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: true)
}
}
let controller = WallpaperGalleryController(context: strongSelf.context, source: source, mode: mode)
controller.requiredLevel = requiredLevel
controller.apply = { [weak self, weak controller] wallpaper, options, editedImage, cropRect, brightness, _ in
if let strongSelf = self {
if case .peer = mode {
var emoticon = ""
if case let .wallpaper(wallpaper, _) = wallpaper {
emoticon = wallpaper.settings?.emoticon ?? ""
}
strongSelf.completion(.emoticon(emoticon))
dismissControllers()
} else {
uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { [weak self, weak controller] in
if let strongSelf = self {
strongSelf.deactivateSearch(animated: false)
strongSelf.controllerNode.scrollToTop(animated: false)
}
if let controller = controller {
switch wallpaper {
case .asset, .contextResult:
controller.dismiss(animated: true)
default:
break
}
}
})
}
}
}
self?.push(controller)
}
}, presentGallery: { [weak self] in
if let strongSelf = self {
let dismissControllers = { [weak self] in
if let self, let navigationController = self.navigationController as? NavigationController {
if case .peer = mode {
var controllers = navigationController.viewControllers.filter({ controller in
if controller is ThemeGridController || controller is MediaPickerScreen {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: false)
controllers = navigationController.viewControllers.filter({ controller in
if controller is WallpaperGalleryController {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: true)
} else {
let controllers = navigationController.viewControllers.filter({ controller in
if controller is WallpaperGalleryController || controller is MediaPickerScreen {
return false
}
return true
})
navigationController.setViewControllers(controllers, animated: true)
}
}
}
let controller = MediaPickerScreenImpl(context: strongSelf.context, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper))
controller.customSelection = { [weak self] _, asset in
guard let strongSelf = self, let asset = asset as? PHAsset else {
return
}
let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: mode)
controller.requiredLevel = requiredCustomLevel
controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, _ in
if let strongSelf = self {
if case .peer = mode {
strongSelf.completion(.custom(wallpaperEntry: wallpaper, options: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness))
Queue.mainQueue().after(0.15) {
dismissControllers()
}
} else {
uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: {
dismissControllers()
})
}
}
}
strongSelf.push(controller)
}
self?.push(controller)
}
}, presentColors: { [weak self] in
if let strongSelf = self {
let controller = ThemeColorsGridController(context: strongSelf.context)
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
}
}, emptyStateUpdated: { [weak self] empty in
if let strongSelf = self {
if empty != strongSelf.isEmpty {
strongSelf.isEmpty = empty
if case .generic = strongSelf.mode {
if empty {
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
} else {
if strongSelf.editingMode {
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed))
} else {
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed))
}
}
}
}
}
}, deleteWallpapers: { [weak self] wallpapers, completed in
if let strongSelf = self {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Wallpaper_DeleteConfirmation(Int32(wallpapers.count)), color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
completed()
guard let strongSelf = self else {
return
}
for wallpaper in wallpapers {
if wallpaper == strongSelf.presentationData.chatWallpaper {
let presentationData = strongSelf.presentationData
let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
let themeReference: PresentationThemeReference
if presentationData.autoNightModeTriggered {
themeReference = current.automaticThemeSwitchSetting.theme
} else {
themeReference = current.theme
}
themeSpecificChatWallpapers[themeReference.index] = nil
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: current.themeSpecificAccentColors[themeReference.index])] = nil
return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers)
})).start()
break
}
}
var deleteWallpapers: [Signal<Void, NoError>] = []
for wallpaper in wallpapers {
deleteWallpapers.append(deleteWallpaper(account: strongSelf.context.account, wallpaper: wallpaper))
}
let _ = (combineLatest(deleteWallpapers)
|> deliverOnMainQueue).start(completed: { [weak self] in
if let strongSelf = self {
strongSelf.controllerNode.updateWallpapers()
}
})
strongSelf.donePressed()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
}
}, shareWallpapers: { [weak self] wallpapers in
if let strongSelf = self {
strongSelf.shareWallpapers(wallpapers)
}
}, resetWallpapers: { [weak self] in
if let strongSelf = self {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
let items: [ActionSheetItem] = [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Wallpaper_ResetWallpapersConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.scrollToTop?()
let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
strongSelf.present(controller, in: .window(.root))
let _ = resetWallpapers(account: strongSelf.context.account).start(completed: { [weak self, weak controller] in
let _ = updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
return current.withUpdatedThemeSpecificChatWallpapers([:])
}).start()
let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in
WallpapersState.update(transaction: transaction, { state in
var state = state
state.wallpapers.removeAll()
return state
})
}).start()
let _ = (telegramWallpapers(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network)
|> deliverOnMainQueue).start(completed: { [weak self, weak controller] in
controller?.dismiss()
if let strongSelf = self {
strongSelf.controllerNode.updateWallpapers()
}
})
})
}
})
]
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
}
}, popViewController: { [weak self] in
if let strongSelf = self {
let _ = (strongSelf.navigationController as? NavigationController)?.popViewController(animated: true)
}
})
self.controllerNode.navigationBar = self.navigationBar
self.controllerNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch(animated: true)
}
self.controllerNode.requestWallpaperRemoval = { [weak self] in
if let self {
self.completion(.remove)
self.dismiss()
}
}
self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateGridVisibleContentOffset(offset)
}
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, max(0.0, value - 54.0)) / 30.0, transition: transition)
case .unknown, .none:
strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
strongSelf.previousContentOffset = offset
}
}
self.controllerNode.gridNode.scrollingCompleted = { [weak self] in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
let _ = strongSelf.controllerNode.fixNavigationSearchableGridNodeScrolling(searchNode: searchContentNode)
}
}
self._ready.set(self.controllerNode.ready.get())
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.displayNodeDidLoad()
}
private func shareWallpapers(_ wallpapers: [TelegramWallpaper]) {
var string: String = ""
for wallpaper in wallpapers {
var item: String?
switch wallpaper {
case let .file(file):
var options: [String] = []
if file.isPattern {
if file.settings.colors.count >= 1 {
options.append("bg_color=\(UIColor(rgb: file.settings.colors[0]).hexString)")
}
if let intensity = file.settings.intensity {
options.append("intensity=\(intensity)")
}
}
var optionsString = ""
if !options.isEmpty {
optionsString = "?\(options.joined(separator: "&"))"
}
item = file.slug + optionsString
case let .color(color):
item = "\(UIColor(rgb: color).hexString)"
default:
break
}
if let item = item {
if !string.isEmpty {
string.append("\n")
}
string.append("https://t.me/bg/\(item)")
}
}
let subject: ShareControllerSubject
if wallpapers.count == 1 {
subject = .url(string)
} else {
subject = .text(string)
}
let shareController = ShareController(context: context, subject: subject)
self.present(shareController, in: .window(.root), blockInteraction: true)
self.donePressed()
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition)
}
func activateSearch() {
if self.displayNavigationBar {
let _ = (self.controllerNode.ready.get()
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
if let scrollToTop = strongSelf.scrollToTop {
scrollToTop()
}
if let searchContentNode = strongSelf.searchContentNode {
strongSelf.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
}
strongSelf.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
})
}
}
func deactivateSearch(animated: Bool) {
if !self.displayNavigationBar {
self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
if let searchContentNode = self.searchContentNode {
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
}
}
}
@objc func editPressed() {
self.editingMode = true
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.searchContentNode?.setIsEnabled(false, animated: true)
self.controllerNode.updateState { state in
var state = state
state.editing = true
return state
}
}
@objc func donePressed() {
self.editingMode = false
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.searchContentNode?.setIsEnabled(true, animated: true)
self.controllerNode.updateState { state in
var state = state
state.editing = false
return state
}
}
}
@@ -0,0 +1,229 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import AccountContext
import GridMessageSelectionNode
import SettingsThemeWallpaperNode
import TelegramPresentationData
private var cachedBorderImages: [String: UIImage] = [:]
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)"
if let image = cachedBorderImages[key] {
return image
} else {
let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let lineWidth: CGFloat
if selected {
lineWidth = 2.0
context.setLineWidth(lineWidth)
context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: 2.0 + lineWidth / 2.0, dy: 2.0 + lineWidth / 2.0))
var accentColor = theme.list.itemAccentColor
if accentColor.rgb == 0xffffff {
accentColor = UIColor(rgb: 0x999999)
}
context.setStrokeColor(accentColor.cgColor)
} else {
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
lineWidth = 1.0
}
if bordered || selected {
context.setLineWidth(lineWidth)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
}
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
cachedBorderImages[key] = image
return image
}
}
final class ThemeGridControllerItem: GridItem {
let context: AccountContext
let theme: PresentationTheme?
let wallpaper: TelegramWallpaper
let wallpaperId: ThemeGridControllerEntry.StableId
let isEmpty: Bool
let emojiFile: TelegramMediaFile?
let channelMode: Bool
let index: Int
let editable: Bool
let selected: Bool
let interaction: ThemeGridControllerInteraction
let section: GridSection? = nil
init(context: AccountContext, theme: PresentationTheme? = nil, wallpaper: TelegramWallpaper, wallpaperId: ThemeGridControllerEntry.StableId, isEmpty: Bool = false, emojiFile: TelegramMediaFile? = nil, channelMode: Bool = false, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) {
self.context = context
self.theme = theme
self.wallpaper = wallpaper
self.wallpaperId = wallpaperId
self.isEmpty = isEmpty
self.emojiFile = emojiFile
self.channelMode = channelMode
self.index = index
self.editable = editable
self.selected = selected
self.interaction = interaction
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ThemeGridControllerItemNode()
node.setup(item: self, synchronousLoad: synchronousLoad)
return node
}
func update(node: GridItemNode) {
guard let node = node as? ThemeGridControllerItemNode else {
assertionFailure()
return
}
node.setup(item: self, synchronousLoad: false)
}
}
final class ThemeGridControllerItemNode: GridItemNode {
private let wallpaperNode: SettingsThemeWallpaperNode
private var selectionNode: GridMessageSelectionNode?
private var selectionBorderNode: ASImageNode?
private var textNode: ImmediateTextNode?
private var item: ThemeGridControllerItem?
override init() {
self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false)
super.init()
self.clipsToBounds = true
self.addSubnode(self.wallpaperNode)
}
override func didLoad() {
super.didLoad()
self.view.layer.cornerRadius = 10.0
self.view.isExclusiveTouch = true
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func setup(item: ThemeGridControllerItem, synchronousLoad: Bool) {
self.item = item
self.updateSelectionState(animated: false)
if item.channelMode, item.selected, let theme = item.theme {
let selectionBorderNode: ASImageNode
if let current = self.selectionBorderNode {
selectionBorderNode = current
} else {
selectionBorderNode = ASImageNode()
selectionBorderNode.displaysAsynchronously = false
self.selectionBorderNode = selectionBorderNode
self.addSubnode(selectionBorderNode)
}
selectionBorderNode.image = generateBorderImage(theme: theme, bordered: true, selected: true)
} else {
self.selectionBorderNode?.removeFromSupernode()
}
if item.channelMode, item.isEmpty, let theme = item.theme {
let textNode: ImmediateTextNode
if let current = self.textNode {
textNode = current
} else {
textNode = ImmediateTextNode()
textNode.maximumNumberOfLines = 2
textNode.textAlignment = .center
self.textNode = textNode
self.addSubnode(textNode)
}
let strings = item.context.sharedContext.currentPresentationData.with { $0 }.strings
textNode.attributedText = NSAttributedString(string: strings.Wallpaper_NoWallpaper, font: Font.regular(15.0), textColor: theme.list.itemSecondaryTextColor)
}
self.setNeedsLayout()
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item, !item.isEmpty {
item.interaction.openWallpaper(item.wallpaper)
}
}
}
func updateSelectionState(animated: Bool) {
if let item = self.item {
let (editing, selectedIds) = item.interaction.selectionState
if editing && item.editable {
let selected = selectedIds.contains(item.wallpaperId)
if let selectionNode = self.selectionNode {
selectionNode.updateSelected(selected, animated: animated)
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
} else {
let theme = item.context.sharedContext.currentPresentationData.with { $0 }.theme
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
if let strongSelf = self {
strongSelf.item?.interaction.toggleWallpaperSelection(item.wallpaperId, value)
}
})
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.addSubnode(selectionNode)
self.selectionNode = selectionNode
selectionNode.updateSelected(selected, animated: false)
if animated {
selectionNode.animateIn()
}
}
}
else {
if let selectionNode = self.selectionNode {
self.selectionNode = nil
if animated {
selectionNode.animateOut { [weak selectionNode] in
selectionNode?.removeFromSupernode()
}
} else {
selectionNode.removeFromSupernode()
}
}
}
}
}
override func layout() {
super.layout()
let bounds = self.bounds
if let item = self.item {
self.wallpaperNode.setWallpaper(context: item.context, theme: item.theme, wallpaper: item.wallpaper, isEmpty: item.isEmpty, emojiFile: item.emojiFile, selected: !item.channelMode && item.selected, size: bounds.size, synchronousLoad: false)
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size)
}
self.selectionBorderNode?.frame = CGRect(origin: CGPoint(), size: bounds.size)
if let textNode = self.textNode {
let textSize = textNode.updateLayout(bounds.size)
textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: floorToScreenPixels((bounds.height - textSize.height) / 2.0) - 18.0), size: textSize)
}
}
}
@@ -0,0 +1,258 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ListSectionHeaderNode
private class ThemeGridColorNode: HighlightableButtonNode {
let action: () -> Void
init(color: WallpaperSearchColor, strokeColor: UIColor, dark: Bool, action: @escaping (WallpaperSearchColor) -> Void) {
self.action = {
action(color)
}
super.init()
let image: UIImage?
if color == .white && !dark {
image = generateFilledCircleImage(diameter: 42.0, color: .white, strokeColor: strokeColor, strokeWidth: 1.0)
} else if color == .black && dark {
image = generateFilledCircleImage(diameter: 42.0, color: .black, strokeColor: strokeColor, strokeWidth: 1.0)
} else {
image = generateFilledCircleImage(diameter: 42.0, color: color.displayColor)
}
self.setImage(image, for: .normal)
}
override func didLoad() {
super.didLoad()
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc func pressed() {
self.action()
}
}
private let inset: CGFloat = 15.0
private let diameter: CGFloat = 42.0
final class ThemeGridSearchColorsNode: ASDisplayNode {
private var theme: PresentationTheme
private var strings: PresentationStrings
private let sectionHeaderNode: ListSectionHeaderNode
private let scrollNode: ASScrollNode
private let colorSelected: (WallpaperSearchColor) -> Void
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, colorSelected: @escaping (WallpaperSearchColor) -> Void) {
self.theme = theme
self.strings = strings
self.colorSelected = colorSelected
self.sectionHeaderNode = ListSectionHeaderNode(theme: theme)
self.sectionHeaderNode.title = strings.WallpaperSearch_ColorTitle.uppercased()
self.scrollNode = ASScrollNode()
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
super.init()
self.addSubnode(self.sectionHeaderNode)
self.addSubnode(self.scrollNode)
self.scrollNode.view.contentSize = CGSize(width: (inset + diameter) * CGFloat(WallpaperSearchColor.allCases.count) + inset, height: 71.0)
for color in WallpaperSearchColor.allCases {
let colorNode = ThemeGridColorNode(color: color, strokeColor: theme.list.controlSecondaryColor, dark: theme.overallDarkAppearance, action: colorSelected)
self.scrollNode.addSubnode(colorNode)
}
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
if self.theme !== theme || self.strings !== strings {
self.theme = theme
self.strings = strings
self.sectionHeaderNode.title = strings.WallpaperSearch_ColorTitle.uppercased()
self.sectionHeaderNode.updateTheme(theme: theme)
}
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 100.0)
}
private var validLayout: (CGSize, CGFloat, CGFloat)?
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
let hadLayout = self.validLayout != nil
self.validLayout = (size, leftInset, rightInset)
self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 29.0))
self.sectionHeaderNode.updateLayout(size: CGSize(width: size.width, height: 29.0), leftInset: leftInset, rightInset: rightInset)
var insets = UIEdgeInsets()
insets.left += leftInset
insets.right += rightInset
self.scrollNode.frame = CGRect(x: 0.0, y: 29.0, width: size.width, height: size.height - 29.0)
self.scrollNode.view.contentInset = insets
if !hadLayout {
self.scrollNode.view.contentOffset = CGPoint(x: -leftInset, y: 0.0)
}
var offset: CGFloat = inset
if let subnodes = self.scrollNode.subnodes {
for node in subnodes {
node.frame = CGRect(x: offset, y: inset, width: diameter, height: diameter)
offset += diameter + inset
}
}
}
}
class ThemeGridSearchColorsItem: ListViewItem {
let account: Account
let theme: PresentationTheme
let strings: PresentationStrings
let colorSelected: (WallpaperSearchColor) -> Void
let header: ListViewItemHeader?
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, colorSelected: @escaping (WallpaperSearchColor) -> Void) {
self.account = account
self.theme = theme
self.strings = strings
self.colorSelected = colorSelected
self.header = nil
}
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 = ThemeGridSearchColorsItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem != nil)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
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 {
if let nodeValue = node() as? ThemeGridSearchColorsItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem != nil)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
}
class ThemeGridSearchColorsItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private var colorsNode: ThemeGridSearchColorsNode?
private var item: ThemeGridSearchColorsItem?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: ThemeGridSearchColorsItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let currentItem = self.item
return { [weak self] item, params, last in
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 101.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor
}
let colorsNode: ThemeGridSearchColorsNode
if let currentColorsNode = strongSelf.colorsNode {
colorsNode = currentColorsNode
colorsNode.updateThemeAndStrings(theme: item.theme, strings: item.strings)
} else {
colorsNode = ThemeGridSearchColorsNode(account: item.account, theme: item.theme, strings: item.strings, colorSelected: item.colorSelected)
strongSelf.colorsNode = colorsNode
strongSelf.addSubnode(colorsNode)
}
colorsNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
colorsNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
let separatorHeight = UIScreenPixel
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = true
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
}
@@ -0,0 +1,834 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import MergeLists
import AccountContext
import SearchUI
import ChatListSearchItemHeader
import WebSearchUI
import SearchBarNode
enum WallpaperSearchColor: CaseIterable {
case blue
case red
case orange
case yellow
case green
case teal
case purple
case pink
case brown
case black
case gray
case white
var string: String {
switch self {
case .blue:
return "Blue"
case .red:
return "Red"
case .orange:
return "Orange"
case .yellow:
return "Yellow"
case .green:
return "Green"
case .teal:
return "Teal"
case .purple:
return "Purple"
case .pink:
return "Pink"
case .brown:
return "Brown"
case .black:
return "Black"
case .gray:
return "Gray"
case .white:
return "White"
}
}
var displayColor: UIColor {
switch self {
case .blue:
return UIColor(rgb: 0x0076ff)
case .red:
return UIColor(rgb: 0xff0000)
case .orange:
return UIColor(rgb: 0xff8a00)
case .yellow:
return UIColor(rgb: 0xffca00)
case .green:
return UIColor(rgb: 0x00e432)
case .teal:
return UIColor(rgb: 0x1fa9ab)
case .purple:
return UIColor(rgb: 0x7300aa)
case .pink:
return UIColor(rgb: 0xf9bec5)
case .brown:
return UIColor(rgb: 0x734021)
case .black:
return UIColor(rgb: 0x000000)
case .gray:
return UIColor(rgb: 0x5c585f)
case .white:
return UIColor(rgb: 0xffffff)
}
}
func localizedString(strings: PresentationStrings) -> String {
switch self {
case .blue:
return strings.WallpaperSearch_ColorBlue
case .red:
return strings.WallpaperSearch_ColorRed
case .orange:
return strings.WallpaperSearch_ColorOrange
case .yellow:
return strings.WallpaperSearch_ColorYellow
case .green:
return strings.WallpaperSearch_ColorGreen
case .teal:
return strings.WallpaperSearch_ColorTeal
case .purple:
return strings.WallpaperSearch_ColorPurple
case .pink:
return strings.WallpaperSearch_ColorPink
case .brown:
return strings.WallpaperSearch_ColorBrown
case .black:
return strings.WallpaperSearch_ColorBlack
case .gray:
return strings.WallpaperSearch_ColorGray
case .white:
return strings.WallpaperSearch_ColorWhite
}
}
}
enum WallpaperSearchQuery: Equatable {
case generic(String)
case color(WallpaperSearchColor, String)
var botQuery: String {
switch self {
case let .generic(query):
return query
case let .color(color, query):
return "#color\(color.string) \(query)"
}
}
var query: String {
switch self {
case let .generic(query), let .color(_, query):
return query
}
}
func updatedWithText(_ text: String) -> WallpaperSearchQuery {
switch self {
case .generic:
return .generic(text)
case let .color(color, _):
return .color(color, text)
}
}
func updatedWithColor(_ color: WallpaperSearchColor?) -> WallpaperSearchQuery {
if let color = color {
switch self {
case let .generic(text):
return .color(color, text)
case let .color(_, text):
return .color(color, text)
}
} else {
switch self {
case .generic:
return self
case let .color(_, text):
return .generic(text)
}
}
}
}
final class ThemeGridSearchInteraction {
let openResult: (ChatContextResult) -> Void
let selectColor: (WallpaperSearchColor) -> Void
let setSearchQuery: (WallpaperSearchQuery) -> Void
let deleteRecentQuery: (String) -> Void
init(openResult: @escaping (ChatContextResult) -> Void, selectColor: @escaping (WallpaperSearchColor) -> Void, setSearchQuery: @escaping (WallpaperSearchQuery) -> Void, deleteRecentQuery: @escaping (String) -> Void) {
self.openResult = openResult
self.selectColor = selectColor
self.setSearchQuery = setSearchQuery
self.deleteRecentQuery = deleteRecentQuery
}
}
private enum ThemeGridRecentEntryStableId: Hashable {
case colors
case query(String)
}
private enum ThemeGridRecentEntry: Comparable, Identifiable {
case colors(PresentationTheme, PresentationStrings)
case query(Int, String)
var stableId: ThemeGridRecentEntryStableId {
switch self {
case .colors:
return .colors
case let .query(_, query):
return .query(query)
}
}
static func ==(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool {
switch lhs {
case let .colors(lhsTheme, lhsStrings):
if case let .colors(rhsTheme, rhsStrings) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
return true
} else {
return false
}
case let .query(lhsIndex, lhsQuery):
if case .query(lhsIndex, lhsQuery) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool {
switch lhs {
case .colors:
return true
case let .query(lhsIndex, _):
switch rhs {
case .colors:
return false
case let .query(rhsIndex, _):
return lhsIndex <= rhsIndex
}
}
}
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ListViewItem {
switch self {
case let .colors(theme, strings):
return ThemeGridSearchColorsItem(account: account, theme: theme, strings: strings, colorSelected: { color in
interaction.selectColor(color)
})
case let .query(_, query):
return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: query, tapped: { query in
interaction.setSearchQuery(.generic(query))
}, deleted: { query in
interaction.deleteRecentQuery(query)
}, header: header)
}
}
}
private struct ThemeGridSearchContainerRecentTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private struct ThemeGridSearchEntry: Comparable, Identifiable {
let index: Int
let result: ChatContextResult
static func ==(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool {
return lhs.index == rhs.index && lhs.result == rhs.result
}
static func <(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool {
return lhs.index < rhs.index
}
var stableId: Int {
return self.index
}
func item(account: Account, theme: PresentationTheme, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchItem {
return ThemeGridSearchItem(account: account, theme: theme, result: self.result, interaction: interaction)
}
}
struct ThemeGridSearchContainerTransition {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let displayingResults: Bool
let isEmpty: Bool
let query: String
}
private func themeGridSearchContainerPreparedRecentTransition(from fromEntries: [ThemeGridRecentEntry], to toEntries: [ThemeGridRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ThemeGridSearchContainerRecentTransition {
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(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) }
return ThemeGridSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
}
private func themeGridSearchContainerPreparedTransition(from fromEntries: [ThemeGridSearchEntry], to toEntries: [ThemeGridSearchEntry], displayingResults: Bool, account: Account, theme: PresentationTheme, isEmpty: Bool, query: String, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interaction: interaction), previousIndex: $0.2) }
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interaction: interaction)) }
return ThemeGridSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, query: query)
}
private struct ThemeGridSearchResult {
let query: String
let collection: ChatContextResultCollection
let items: [ChatContextResult]
let nextOffset: String?
}
private struct ThemeGridSearchContext {
let result: ThemeGridSearchResult
let loadMoreIndex: String?
}
final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let recentListNode: ListView
private let gridNode: GridNode
private let dimNode: ASDisplayNode
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private var enqueuedRecentTransitions: [(ThemeGridSearchContainerRecentTransition, Bool)] = []
private var enqueuedTransitions: [(ThemeGridSearchContainerTransition, Bool)] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private var queryValue: WallpaperSearchQuery = .generic("")
private let queryPromise: Promise<WallpaperSearchQuery>
private let searchDisposable = MetaDisposable()
private var recentDisposable: Disposable?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
private let _isSearching = ValuePromise<Bool>(false, ignoreRepeated: true)
override var isSearching: Signal<Bool, NoError> {
return self._isSearching.get()
}
init(context: AccountContext, openResult: @escaping (ChatContextResult) -> Void) {
self.context = context
self.queryPromise = Promise<WallpaperSearchQuery>(self.queryValue)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.recentListNode = ListView()
self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.recentListNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.gridNode = GridNode()
self.emptyResultsTitleNode = ImmediateTextNode()
self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor)
self.emptyResultsTitleNode.textAlignment = .center
self.emptyResultsTitleNode.isHidden = true
self.emptyResultsTextNode = ImmediateTextNode()
self.emptyResultsTextNode.maximumNumberOfLines = 0
self.emptyResultsTextNode.textAlignment = .center
self.emptyResultsTextNode.isHidden = true
super.init()
self.dimNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.dimNode)
self.addSubnode(self.recentListNode)
self.addSubnode(self.gridNode)
self.addSubnode(self.emptyResultsTitleNode)
self.addSubnode(self.emptyResultsTextNode)
let searchContext = Promise<ThemeGridSearchContext?>(nil)
let searchContextValue = Atomic<ThemeGridSearchContext?>(value: nil)
let updateSearchContext: ((ThemeGridSearchContext?) -> (ThemeGridSearchContext?, Bool)) -> Void = { f in
var shouldUpdate = false
let updated = searchContextValue.modify { current in
let (u, s) = f(current)
shouldUpdate = s
if s {
return u
} else {
return current
}
}
if shouldUpdate {
searchContext.set(.single(updated))
}
}
self.gridNode.isHidden = true
self.gridNode.visibleItemsUpdated = { visibleItems in
if let bottom = visibleItems.bottom {
if let context = searchContextValue.with({ $0 }), bottom.0 >= context.result.items.count - 8 {
updateSearchContext { previous in
guard let previous = previous else {
return (nil, false)
}
if previous.loadMoreIndex != nil {
return (previous, false)
}
guard let _ = previous.result.items.last else {
return (previous, false)
}
return (ThemeGridSearchContext(result: previous.result, loadMoreIndex: previous.result.nextOffset), true)
}
}
}
}
self.recentListNode.isHidden = false
let previousSearchItems = Atomic<[ThemeGridSearchEntry]?>(value: nil)
let interaction = ThemeGridSearchInteraction(openResult: { [weak self] result in
openResult(result)
if let strongSelf = self {
strongSelf.dismissInput?()
let query = strongSelf.queryValue.query
if !query.isEmpty {
let _ = addRecentWallpaperSearchQuery(engine: strongSelf.context.engine, string: query).start()
}
}
}, selectColor: { [weak self] color in
self?.updateQuery({ $0.updatedWithColor(color) }, updateInterface: true)
}, setSearchQuery: { [weak self] query in
self?.dismissInput?()
self?.updateQuery({ _ in
return query
}, updateInterface: true)
}, deleteRecentQuery: { query in
let _ = removeRecentWallpaperSearchQuery(engine: context.engine, string: query).start()
})
let configuration = self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
let foundItems = self.queryPromise.get()
|> mapToSignal { query -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in
let query = query.botQuery
guard !query.isEmpty else {
return .single(nil)
}
let wallpaperQuery = "#wallpaper \(query)"
updateSearchContext { _ in
return (nil, true)
}
return .single(([], true))
|> then(
configuration
|> mapToSignal { configuration -> Signal<Peer?, NoError> in
guard let name = configuration.imageBotUsername else {
return .single(nil)
}
return context.engine.peers.resolvePeerByName(name: name, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<Peer?, NoError> in
if let peer = peer {
return .single(peer._asPeer())
} else {
return .single(nil)
}
}
}
|> mapToSignal { peer -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
let loadMore = searchContext.get()
|> mapToSignal { searchContext -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in
if let searchContext = searchContext {
if let _ = searchContext.loadMoreIndex, let nextOffset = searchContext.result.nextOffset {
let collection = searchContext.result.collection
let geoPoint = collection.geoPoint.flatMap { geoPoint -> (Double, Double) in
return (geoPoint.latitude, geoPoint.longitude)
}
return self.context.engine.messages.requestChatContextResults(botId: collection.botId, peerId: collection.peerId, query: searchContext.result.query, location: .single(geoPoint), offset: nextOffset)
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
|> map { nextResults -> (ChatContextResultCollection, String?) in
var results: [ChatContextResult] = []
var existingIds = Set<String>()
for result in searchContext.result.items {
results.append(result)
existingIds.insert(result.id)
}
var nextOffset: String?
if let nextResults = nextResults {
for result in nextResults.results {
if !existingIds.contains(result.id) {
results.append(result)
existingIds.insert(result.id)
}
}
if let newNextOffset = nextResults.nextOffset, !newNextOffset.isEmpty {
nextOffset = newNextOffset
}
}
let merged = ChatContextResultCollection(botId: collection.botId, peerId: collection.peerId, query: collection.query, geoPoint: collection.geoPoint, queryId: nextResults?.queryId ?? collection.queryId, nextOffset: nextOffset ?? "", presentation: collection.presentation, switchPeer: collection.switchPeer, webView: collection.webView, results: results, cacheTimeout: collection.cacheTimeout)
return (merged, nextOffset)
}
|> mapToSignal { newCollection, nextOffset -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in
updateSearchContext { previous in
return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: searchContext.result.query, collection: newCollection, items: newCollection.results, nextOffset: nextOffset), loadMoreIndex: nil), true)
}
return .complete()
}
} else {
var entries: [ThemeGridSearchEntry] = []
var i = 0
for result in searchContext.result.items {
entries.append(ThemeGridSearchEntry(index: i, result: result))
i += 1
}
return .single((entries, false))
}
} else {
return .complete()
}
}
return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue()))
|> then(
requestContextResults(engine: context.engine, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16)
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> map { collection -> ([ThemeGridSearchEntry], Bool)? in
guard let collection = collection else {
return nil
}
var entries: [ThemeGridSearchEntry] = []
var i = 0
for result in collection.results {
entries.append(ThemeGridSearchEntry(index: i, result: result))
i += 1
}
updateSearchContext { _ in
return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: wallpaperQuery, collection: collection, items: collection.results, nextOffset: collection.nextOffset), loadMoreIndex: nil), true)
}
return (entries, false)
}
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
|> then(loadMore)
)
} else {
return .single(nil)
}
}
)
}
let previousRecentItems = Atomic<[ThemeGridRecentEntry]?>(value: nil)
self.recentDisposable = (combineLatest(wallpaperSearchRecentQueries(engine: self.context.engine), self.presentationDataPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] queries, presentationData in
if let strongSelf = self {
var entries: [ThemeGridRecentEntry] = []
entries.append(.colors(presentationData.theme, presentationData.strings))
for i in 0 ..< queries.count {
entries.append(.query(i, queries[i]))
}
let header = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: { _ in
let _ = clearRecentWallpaperSearchQueries(engine: strongSelf.context.engine).start()
})
let previousEntries = previousRecentItems.swap(entries)
let transition = themeGridSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, header: header)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
})
self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get(), self.queryPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags, presentationData, query in
if let strongSelf = self {
strongSelf._isSearching.set(entriesAndFlags?.1 ?? false)
let previousEntries = previousSearchItems.swap(entriesAndFlags?.0)
var isEmpty = false
if let entriesAndFlags = entriesAndFlags {
isEmpty = entriesAndFlags.0.isEmpty && !entriesAndFlags.1
}
let firstTime = previousEntries == nil
let transition = themeGridSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, account: context.account, theme: presentationData.theme, isEmpty: isEmpty, query: query.query, interaction: interaction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
strongSelf.presentationData = presentationData
strongSelf.presentationDataPromise.set(.single(presentationData))
if previousTheme !== presentationData.theme {
strongSelf.updateTheme(theme: presentationData.theme)
}
}
})
self.recentListNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
self.gridNode.scrollingInitiated = { [weak self] in
self?.dismissInput?()
}
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
deinit {
self.searchDisposable.dispose()
self.recentDisposable?.dispose()
self.presentationDataDisposable?.dispose()
}
private func updateTheme(theme: PresentationTheme) {
self.backgroundColor = theme.chatList.backgroundColor
self.dimNode.backgroundColor = theme.chatList.backgroundColor
self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
}
private func updateQuery(_ f: (WallpaperSearchQuery) -> (WallpaperSearchQuery), updateInterface: Bool = false) {
let query = f(self.queryValue)
if query != self.queryValue {
self.queryValue = query
self.queryPromise.set(.single(query))
if updateInterface {
let tokens: [SearchBarToken]
let text: String
let placeholder: String
switch query {
case let .generic(query):
tokens = []
text = query
placeholder = self.presentationData.strings.Wallpaper_Search
case let .color(color, query):
let backgroundColor = color.displayColor
let foregroundColor: UIColor
let strokeColor: UIColor
if color == .white {
foregroundColor = .black
strokeColor = self.presentationData.theme.rootController.navigationSearchBar.inputClearButtonColor
} else {
foregroundColor = .white
strokeColor = color.displayColor
}
tokens = [SearchBarToken(id: 0, icon: UIImage(bundleImageName: "Settings/WallpaperSearchColorIcon"), title: color.localizedString(strings: self.presentationData.strings), style: SearchBarToken.Style(backgroundColor: backgroundColor, foregroundColor: foregroundColor, strokeColor: strokeColor), permanent: false)]
text = query
placeholder = self.presentationData.strings.Wallpaper_SearchShort
}
self.setQuery?(nil, tokens, text)
self.setPlaceholder?(placeholder)
}
}
}
override func searchTextUpdated(text: String) {
self.updateQuery({ $0.updatedWithText(text) })
}
override func searchTextClearPrefix() {
self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true)
}
override func searchTextClearTokens() {
self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true)
}
private func enqueueRecentTransition(_ transition: ThemeGridSearchContainerRecentTransition, firstTime: Bool) {
self.enqueuedRecentTransitions.append((transition, firstTime))
if self.validLayout != nil {
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
}
}
private func dequeueRecentTransition() {
if let (transition, firstTime) = self.enqueuedRecentTransitions.first {
self.enqueuedRecentTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if firstTime {
options.insert(.PreferSynchronousDrawing)
} else {
options.insert(.AnimateInsertion)
}
self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
}
private func enqueueTransition(_ transition: ThemeGridSearchContainerTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if self.validLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
let displayingResults = transition.displayingResults
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.gridNode.isHidden = !displayingResults
strongSelf.recentListNode.isHidden = displayingResults
strongSelf.dimNode.isHidden = displayingResults
strongSelf.backgroundColor = strongSelf.presentationData.theme.chatList.backgroundColor
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.WebSearch_SearchNoResultsDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
let emptyResults = displayingResults && transition.isEmpty
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
})
}
}
override 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 minSpacing: CGFloat = 8.0
let referenceImageSize: CGSize
let screenWidth = min(layout.size.width, layout.size.height)
if screenWidth >= 390.0 {
referenceImageSize = CGSize(width: 108.0, height: 230.0)
} else {
referenceImageSize = CGSize(width: 91.0, height: 161.0)
}
let imageCount = Int((layout.size.width - minSpacing * 2.0) / (referenceImageSize.width + minSpacing))
let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height))
let spacing = floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1))
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)))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight + spacing, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: 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 insets = layout.insets(options: [.input])
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.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func clearRecentSearch() {
let _ = (self.context.engine.peers.clearRecentlySearchedPeers() |> deliverOnMainQueue).start()
}
override func scrollToTop() {
if !self.gridNode.isHidden {
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
} else {
self.recentListNode.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 })
}
}
}
@@ -0,0 +1,155 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import TelegramPresentationData
import PhotoResources
final class ThemeGridSearchItem: GridItem {
let account: Account
let theme: PresentationTheme
let result: ChatContextResult
let interaction: ThemeGridSearchInteraction
let section: GridSection? = nil
init(account: Account, theme: PresentationTheme, result: ChatContextResult, interaction: ThemeGridSearchInteraction) {
self.account = account
self.theme = theme
self.result = result
self.interaction = interaction
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ThemeGridSearchItemNode()
node.setup(item: self)
return node
}
func update(node: GridItemNode) {
guard let node = node as? ThemeGridSearchItemNode else {
assertionFailure()
return
}
node.setup(item: self)
}
}
final class ThemeGridSearchItemNode: GridItemNode {
private let imageNode: TransformImageNode
private(set) var item: ThemeGridSearchItem?
private var currentDimensions: CGSize?
override init() {
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.imageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.imageNode)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func setup(item: ThemeGridSearchItem) {
if self.item !== item {
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var thumbnailDimensions: CGSize?
var thumbnailResource: TelegramMediaResource?
var imageResource: TelegramMediaResource?
var imageDimensions: CGSize?
var immediateThumbnailData: Data?
switch item.result {
case let .externalReference(externalReference):
if let content = externalReference.content, externalReference.type != "gif" {
imageResource = content.resource
} else if let thumbnail = externalReference.thumbnail {
imageResource = thumbnail.resource
}
imageDimensions = externalReference.content?.dimensions?.cgSize
case let .internalReference(internalReference):
if let image = internalReference.image {
immediateThumbnailData = image.immediateThumbnailData
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 321, height: 321)) {
imageResource = representation.resource
imageDimensions = representation.dimensions.cgSize
}
if let file = internalReference.file {
if let thumbnailRepresentation = smallestImageRepresentation(file.previewRepresentations) {
thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize
thumbnailResource = thumbnailRepresentation.resource
}
} else {
if let thumbnailRepresentation = smallestImageRepresentation(image.representations) {
thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize
thumbnailResource = thumbnailRepresentation.resource
}
}
} else if let file = internalReference.file {
immediateThumbnailData = file.immediateThumbnailData
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = largestRepresentation.dimensions.cgSize
}
imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource
}
}
var representations: [TelegramMediaImageRepresentation] = []
if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions {
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
if let imageResource = imageResource, let imageDimensions = imageDimensions {
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
if !representations.isEmpty {
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
updateImageSignal = mediaGridMessagePhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage), fullRepresentationSize: CGSize(width: 512, height: 512))
} else {
updateImageSignal = .complete()
}
if let updateImageSignal = updateImageSignal {
self.imageNode.setSignal(updateImageSignal)
}
self.currentDimensions = imageDimensions
if let _ = imageDimensions {
self.setNeedsLayout()
}
}
self.item = item
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item {
item.interaction.openResult(item.result)
}
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.imageNode.frame = bounds
if let item = self.item, let dimensions = self.currentDimensions {
let imageSize = dimensions.aspectFilled(bounds.size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: bounds.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))()
}
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AppBundle
final class ThemeGridSelectionPanelNode: ASDisplayNode {
private let deleteButton: UIButton
private let shareButton: UIButton
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics)?
private var theme: PresentationTheme
var selectedIds = Set<ThemeGridControllerEntry.StableId>() {
didSet {
if oldValue != self.selectedIds {
self.deleteButton.isEnabled = !self.selectedIds.isEmpty
self.shareButton.isEnabled = !self.selectedIds.isEmpty
}
}
}
var controllerInteraction: ThemeGridControllerInteraction?
init(theme: PresentationTheme) {
self.theme = theme
self.deleteButton = UIButton()
self.deleteButton.isEnabled = false
self.shareButton = UIButton()
self.shareButton.isEnabled = false
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
super.init()
self.view.addSubview(self.deleteButton)
self.view.addSubview(self.shareButton)
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), for: [.touchUpInside])
}
func updateTheme(theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
}
}
@objc func deleteButtonPressed() {
self.controllerInteraction?.deleteSelectedWallpapers()
}
@objc func shareButtonPressed() {
self.controllerInteraction?.shareSelectedWallpapers()
}
private func defaultHeight(metrics: LayoutMetrics) -> CGFloat {
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
return 49.0
} else {
return 45.0
}
}
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, metrics: LayoutMetrics) -> CGFloat {
self.validLayout = (width, leftInset, rightInset, maxHeight, metrics)
let panelHeight = defaultHeight(metrics: metrics)
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: panelHeight))
self.shareButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
return panelHeight
}
}
@@ -0,0 +1,68 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramUIPreferences
private struct WallpaperSearchRecentQueryItemId {
public let rawValue: MemoryBuffer
var value: String {
return String(data: self.rawValue.makeData(), encoding: .utf8) ?? ""
}
init(_ rawValue: MemoryBuffer) {
self.rawValue = rawValue
}
init?(_ value: String) {
if let data = value.data(using: .utf8) {
self.rawValue = MemoryBuffer(data: data)
} else {
return nil
}
}
}
public final class RecentWallpaperSearchQueryItem: Codable {
public init() {
}
public init(from decoder: Decoder) throws {
}
public func encode(to encoder: Encoder) throws {
}
}
func addRecentWallpaperSearchQuery(engine: TelegramEngine, string: String) -> Signal<Never, NoError> {
if let itemId = WallpaperSearchRecentQueryItemId(string) {
return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries, id: itemId.rawValue, item: RecentWallpaperSearchQueryItem(), removeTailIfCountExceeds: 100)
} else {
return .complete()
}
}
func removeRecentWallpaperSearchQuery(engine: TelegramEngine, string: String) -> Signal<Never, NoError> {
if let itemId = WallpaperSearchRecentQueryItemId(string) {
return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries, id: itemId.rawValue)
} else {
return .complete()
}
}
func clearRecentWallpaperSearchQueries(engine: TelegramEngine) -> Signal<Never, NoError> {
return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries)
}
func wallpaperSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> {
return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries))
|> map { items -> [String] in
var result: [String] = []
for item in items {
let value = WallpaperSearchRecentQueryItemId(item.id).value
result.append(value)
}
return result
}
}
@@ -0,0 +1,377 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MediaResources
import LocalMediaResources
import TelegramUIPreferences
import AccountContext
import LegacyComponents
import WallpaperGalleryScreen
import ImageBlur
public func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, completion: @escaping () -> Void) {
var imageSignal: Signal<UIImage, NoError>
switch wallpaper {
case let .wallpaper(wallpaper, _):
switch wallpaper {
case let .file(file):
if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data)
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
}
case let .image(representations, _):
for representation in representations {
let resource = representation.resource
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data)
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
}
}
default:
break
}
imageSignal = .complete()
completion()
case let .asset(asset):
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|> filter { value in
return !(value?.1 ?? true)
}
|> mapToSignal { result -> Signal<UIImage, NoError> in
if let result = result {
return .single(result.0)
} else {
return .complete()
}
}
case let .contextResult(result):
var imageResource: TelegramMediaResource?
switch result {
case let .externalReference(externalReference):
if let content = externalReference.content {
imageResource = content.resource
}
case let .internalReference(internalReference):
if let image = internalReference.image {
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
imageResource = imageRepresentation.resource
}
}
}
if let imageResource = imageResource {
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|> mapToSignal { path -> Signal<UIImage, NoError> in
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
return .single(image)
} else {
return .complete()
}
}
} else {
imageSignal = .complete()
}
}
if let editedImage {
imageSignal = .single(editedImage)
}
let _ = (imageSignal
|> map { image -> UIImage in
var croppedImage = UIImage()
let finalCropRect: CGRect
if let cropRect = cropRect {
finalCropRect = cropRect
} else {
let screenSize = TGScreenSize()
let fittedSize = TGScaleToFit(screenSize, image.size)
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
}
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered
let accountManager = context.sharedContext.accountManager
let account = context.account
let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in
var resource: MediaResource?
if case let .image(representations, _) = wallpaper, let representation = largestImageRepresentation(representations) {
resource = representation.resource
} else if case let .file(file) = wallpaper {
resource = file.file.resource
}
if let resource = resource {
let _ = accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
let _ = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
}
let _ = (updatePresentationThemeSettingsInteractively(accountManager: accountManager, { current in
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
let themeReference: PresentationThemeReference
if autoNightModeTriggered {
themeReference = current.automaticThemeSwitchSetting.theme
} else {
themeReference = current.theme
}
let accentColor = current.themeSpecificAccentColors[themeReference.index]
if let accentColor = accentColor, accentColor.baseColor == .custom {
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper
} else {
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil
themeSpecificChatWallpapers[themeReference.index] = wallpaper
}
return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers)
})).start()
}
let apply: () -> Void = {
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil)
let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
updateWallpaper(wallpaper)
DispatchQueue.main.async {
completion()
}
}
if mode.contains(.blur) {
let representation = CachedBlurredWallpaperRepresentation()
let _ = context.account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start()
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start(completed: {
apply()
})
} else {
apply()
}
}
return croppedImage
}).start()
}
public func getTemporaryCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?) -> Signal<TelegramWallpaper?, NoError> {
var imageSignal: Signal<UIImage, NoError>
switch wallpaper {
case .wallpaper:
fatalError()
case let .asset(asset):
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|> filter { value in
return !(value?.1 ?? true)
}
|> mapToSignal { result -> Signal<UIImage, NoError> in
if let result = result {
return .single(result.0)
} else {
return .complete()
}
}
case let .contextResult(result):
var imageResource: TelegramMediaResource?
switch result {
case let .externalReference(externalReference):
if let content = externalReference.content {
imageResource = content.resource
}
case let .internalReference(internalReference):
if let image = internalReference.image {
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
imageResource = imageRepresentation.resource
}
}
}
if let imageResource = imageResource {
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|> mapToSignal { path -> Signal<UIImage, NoError> in
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
return .single(image)
} else {
return .complete()
}
}
} else {
imageSignal = .complete()
}
}
if let editedImage {
imageSignal = .single(editedImage)
}
return imageSignal
|> map { image -> TelegramWallpaper? in
var croppedImage = UIImage()
let finalCropRect: CGRect
if let cropRect = cropRect {
finalCropRect = cropRect
} else {
let screenSize = TGScreenSize()
let fittedSize = TGScaleToFit(screenSize, image.size)
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
}
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
var intensity: Int32?
if let brightness {
intensity = max(0, min(100, Int32(brightness * 100.0)))
}
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity)
let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
return temporaryWallpaper
}
return nil
}
}
public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, peerId: PeerId, forBoth: Bool, completion: @escaping () -> Void) {
var imageSignal: Signal<UIImage, NoError>
switch wallpaper {
case let .wallpaper(wallpaper, _):
imageSignal = .complete()
switch wallpaper {
case let .file(file):
if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) {
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data, synchronous: true)
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
imageSignal = .single(image)
}
case let .image(representations, _):
for representation in representations {
let resource = representation.resource
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
}
}
default:
break
}
completion()
case let .asset(asset):
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|> filter { value in
return !(value?.1 ?? true)
}
|> mapToSignal { result -> Signal<UIImage, NoError> in
if let result = result {
return .single(result.0)
} else {
return .complete()
}
}
case let .contextResult(result):
var imageResource: TelegramMediaResource?
switch result {
case let .externalReference(externalReference):
if let content = externalReference.content {
imageResource = content.resource
}
case let .internalReference(internalReference):
if let image = internalReference.image {
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
imageResource = imageRepresentation.resource
}
}
}
if let imageResource = imageResource {
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|> mapToSignal { path -> Signal<UIImage, NoError> in
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
return .single(image)
} else {
return .complete()
}
}
} else {
imageSignal = .complete()
}
}
if let editedImage {
imageSignal = .single(editedImage)
}
let _ = (imageSignal
|> map { image -> UIImage in
var croppedImage = UIImage()
let finalCropRect: CGRect
if let cropRect = cropRect {
finalCropRect = cropRect
} else {
let screenSize = TGScreenSize()
let fittedSize = TGScaleToFit(screenSize, image.size)
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
}
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
var intensity: Int32?
if let brightness {
intensity = max(0, min(100, Int32(brightness * 100.0)))
}
Queue.mainQueue().after(0.05) {
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity)
let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
context.account.pendingPeerMediaUploadManager.add(peerId: peerId, content: .wallpaper(wallpaper: temporaryWallpaper, forBoth: forBoth))
Queue.mainQueue().after(0.05) {
completion()
}
}
}
return croppedImage
}).start()
}